From 99d25bfd560239c6ddbfcfb130a63079e07b64af Mon Sep 17 00:00:00 2001 From: schizza Date: Thu, 24 Apr 2025 17:56:47 +0200 Subject: [PATCH 01/30] Adds unit migration functionality to options flow Implements a user interface to migrate units for rain sensors including migration of historic data via statistics. This provides the user with the ability to correct rain units, if they have been set incorrectly. Includes UI to select sensor and units, as well as trigger migration. --- custom_components/sws12500/config_flow.py | 188 +++++++++++++++++++--- 1 file changed, 162 insertions(+), 26 deletions(-) diff --git a/custom_components/sws12500/config_flow.py b/custom_components/sws12500/config_flow.py index 508d01e..9e6b2b2 100644 --- a/custom_components/sws12500/config_flow.py +++ b/custom_components/sws12500/config_flow.py @@ -1,13 +1,15 @@ """Config flow for Sencor SWS 12500 Weather Station integration.""" +import logging from typing import Any import voluptuous as vol from homeassistant.config_entries import ConfigFlow, OptionsFlow +from homeassistant.const import UnitOfPrecipitationDepth, UnitOfVolumetricFlux from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from .utils import long_term_units_in_statistics_meta, migrate_data +import homeassistant.helpers.entity_registry as er from .const import ( API_ID, @@ -15,13 +17,18 @@ from .const import ( DEV_DBG, DOMAIN, INVALID_CREDENTIALS, - SENSORS_TO_LOAD, + MIG_FROM, + MIG_TO, SENSOR_TO_MIGRATE, + SENSORS_TO_LOAD, WINDY_API_KEY, WINDY_ENABLED, WINDY_LOGGER_ENABLED, WSLINK, ) +from .utils import long_term_units_in_statistics_meta, migrate_data + +_LOGGER = logging.getLogger(__name__) class CannotConnect(HomeAssistantError): @@ -41,16 +48,23 @@ class ConfigOptionsFlowHandler(OptionsFlow): self.windy_data: dict[str, Any] = {} self.windy_data_schema = {} - self.user_data: dict[str, str] = {} + self.user_data: dict[str, Any] = {} self.user_data_schema = {} self.sensors: dict[str, Any] = {} self.migrate_schema = {} + self.migrate_sensor_select = {} + self.migrate_unit_selection = {} + self.count = 0 + self.selected_sensor = "" + + self.unit_values = [unit.value for unit in UnitOfVolumetricFlux] + self.unit_values.extend([unit.value for unit in UnitOfPrecipitationDepth]) @property def config_entry(self): return self.hass.config_entries.async_get_entry(self.handler) - def _get_entry_data(self): + async def _get_entry_data(self): """Get entry data.""" self.user_data: dict[str, Any] = { @@ -61,10 +75,10 @@ class ConfigOptionsFlowHandler(OptionsFlow): } self.user_data_schema = { - vol.Required(API_ID, default=self.user_data[API_ID] or ""): str, - vol.Required(API_KEY, default=self.user_data[API_KEY] or ""): str, - vol.Optional(WSLINK, default=self.user_data[WSLINK]): bool or False, - vol.Optional(DEV_DBG, default=self.user_data[DEV_DBG]): bool or False, + vol.Required(API_ID, default=self.user_data.get(API_ID, "")): str, + vol.Required(API_KEY, default=self.user_data.get(API_KEY, "")): str, + vol.Optional(WSLINK, default=self.user_data.get(WSLINK, False)): bool, + vol.Optional(DEV_DBG, default=self.user_data.get(DEV_DBG, False)): bool, } self.sensors: dict[str, Any] = { @@ -83,7 +97,7 @@ class ConfigOptionsFlowHandler(OptionsFlow): self.windy_data_schema = { vol.Optional( - WINDY_API_KEY, default=self.windy_data[WINDY_API_KEY] or "" + WINDY_API_KEY, default=self.windy_data.get(WINDY_API_KEY, "") ): str, vol.Optional(WINDY_ENABLED, default=self.windy_data[WINDY_ENABLED]): bool or False, @@ -93,12 +107,34 @@ class ConfigOptionsFlowHandler(OptionsFlow): ): bool or False, } - self.migrate_schema = { + self.migrate_sensor_select = { vol.Required(SENSOR_TO_MIGRATE): vol.In( - long_term_units_in_statistics_meta() or {} + await self.load_sensors_to_migrate() or {} ), + } + + self.migrate_unit_selection = { + vol.Required(MIG_FROM): vol.In(self.unit_values), + vol.Required(MIG_TO): vol.In(self.unit_values), vol.Optional("trigger_action", default=False): bool, } + # "mm/d", "mm/h", "mm", "in/d", "in/h", "in" + + async def load_sensors_to_migrate(self) -> dict[str, Any]: + """Load sensors to migrate.""" + + sensor_statistics = await long_term_units_in_statistics_meta(self.hass) + + entity_registry = er.async_get(self.hass) + sensors = entity_registry.entities.get_entries_for_config_entry_id( + self.config_entry.entry_id + ) + + return { + sensor.entity_id: f"{sensor.name or sensor.original_name} (current settings: {sensor.unit_of_measurement}, longterm stats unit: {sensor_statistics.get(sensor.entity_id)})" + for sensor in sensors + if sensor.unique_id in {"rain", "daily_rain"} + } async def async_step_init(self, user_input=None): """Manage the options - show menu first.""" @@ -110,7 +146,7 @@ class ConfigOptionsFlowHandler(OptionsFlow): """Manage basic options - credentials.""" errors = {} - self._get_entry_data() + await self._get_entry_data() if user_input is None: return self.async_show_form( @@ -147,7 +183,7 @@ class ConfigOptionsFlowHandler(OptionsFlow): """Manage windy options.""" errors = {} - self._get_entry_data() + await self._get_entry_data() if user_input is None: return self.async_show_form( @@ -160,7 +196,7 @@ class ConfigOptionsFlowHandler(OptionsFlow): errors[WINDY_API_KEY] = "windy_key_required" return self.async_show_form( step_id="windy", - data_schema=self.windy_data_schema, + data_schema=vol.Schema(self.windy_data_schema), errors=errors, ) @@ -175,15 +211,17 @@ class ConfigOptionsFlowHandler(OptionsFlow): async def async_step_migration(self, user_input=None): """Migrate sensors.""" - # hj errors = {} - self._get_entry_data() + data_schema = vol.Schema(self.migrate_sensor_select) + data_schema.schema.update() + + await self._get_entry_data() if user_input is None: return self.async_show_form( step_id="migration", - data_schema=vol.Schema(self.migrate_schema), + data_schema=vol.Schema(self.migrate_sensor_select), errors=errors, description_placeholders={ "migration_status": "-", @@ -191,18 +229,116 @@ class ConfigOptionsFlowHandler(OptionsFlow): }, ) - if user_input.get("trigger_action"): - # Akce se vykoná po zaškrtnutí - count = await self.hass.async_add_executor_job( - migrate_data, user_input.get(SENSOR_TO_MIGRATE) - ) + self.selected_sensor = user_input.get(SENSOR_TO_MIGRATE) + + return await self.async_step_migration_units() + + async def async_step_migration_units(self, user_input=None): + """Migrate unit step.""" + + registry = er.async_get(self.hass) + sensor_entry = registry.async_get(self.selected_sensor) + sensor_stats = await long_term_units_in_statistics_meta(self.hass) + + default_unit = sensor_entry.unit_of_measurement if sensor_entry else None + + if default_unit not in self.unit_values: + default_unit = self.unit_values[0] + + data_schema = vol.Schema({ + vol.Required(MIG_FROM, default=default_unit): vol.In(self.unit_values), + vol.Required(MIG_TO): vol.In(self.unit_values), + vol.Optional("trigger_action", default=False): bool, + }) + + if user_input is None: return self.async_show_form( - step_id="migration", - data_schema=vol.Schema(self.migrate_schema), + step_id="migration_units", + data_schema=data_schema, + errors={}, + description_placeholders={ + "migration_sensor": sensor_entry.original_name, + "migration_stats": sensor_stats.get(self.selected_sensor), + }, + ) + + if user_input.get("trigger_action"): + self.count = await migrate_data( + self.hass, + self.selected_sensor, + user_input.get(MIG_FROM), + user_input.get(MIG_TO), + ) + + registry.async_update_entity(self.selected_sensor, + unit_of_measurement=user_input.get(MIG_TO), + ) + + state = self.hass.states.get(self.selected_sensor) + if state: + _LOGGER.info("State attributes before update: %s", state.attributes) + attributes = dict(state.attributes) + attributes["unit_of_measurement"] = user_input.get(MIG_TO) + self.hass.states.async_set(self.selected_sensor, state.state, attributes) + _LOGGER.info("State attributes after update: %s", attributes) + + options = {**self.config_entry.options, "reload_sensor": self.selected_sensor} + self.hass.config_entries.async_update_entry(self.config_entry, options=options) + + await self.hass.config_entries.async_reload(self.config_entry.entry_id) + + await self.hass.async_block_till_done() + + _LOGGER.info("Migration complete for sensor %s: %s row updated, new measurement unit: %s, ", + self.selected_sensor, + self.count, + user_input.get(MIG_TO), + ) + + await self._get_entry_data() + sensor_entry = er.async_get(self.hass).async_get(self.selected_sensor) + sensor_stat = await self.load_sensors_to_migrate() + + return self.async_show_form( + step_id="migration_complete", + data_schema=vol.Schema({}), + errors={}, + description_placeholders={ + "migration_sensor": sensor_entry.unit_of_measurement, + "migration_stats": sensor_stat.get(self.selected_sensor), + "migration_count": self.count, + }, + ) + + # retain windy data + user_input.update(self.windy_data) + + # retain user_data + user_input.update(self.user_data) + + # retain senors + user_input.update(self.sensors) + + return self.async_create_entry(title=DOMAIN, data=user_input) + + async def async_step_migration_complete(self, user_input=None): + """Migration complete.""" + + errors = {} + + await self._get_entry_data() + sensor_entry = er.async_get(self.hass).async_get(self.selected_sensor) + sensor_stat = await self.load_sensors_to_migrate() + + if user_input is None: + return self.async_show_form( + step_id="migration_complete", + data_schema=vol.Schema({}), errors=errors, description_placeholders={ - "migration_status": user_input.get(SENSOR_TO_MIGRATE), - "migration_count": count, + "migration_sensor": sensor_entry.unit_of_measurement, + "migration_stats": sensor_stat.get(self.selected_sensor), + "migration_count": self.count, }, ) From feed73081843007f5dccf299c4e27c92bb4e3b1c Mon Sep 17 00:00:00 2001 From: Ferron Nijland Date: Wed, 9 Jul 2025 16:26:18 +0200 Subject: [PATCH 02/30] Add outside battery sensor and related translations --- custom_components/sws12500/const.py | 33 +++++++++++++++++++ custom_components/sws12500/sensors_wslink.py | 11 ++++++- custom_components/sws12500/strings.json | 1 + .../sws12500/translations/cs.json | 1 + .../sws12500/translations/en.json | 2 ++ custom_components/sws12500/utils.py | 13 ++++++++ 6 files changed, 60 insertions(+), 1 deletion(-) diff --git a/custom_components/sws12500/const.py b/custom_components/sws12500/const.py index 1819c7f..28bc375 100644 --- a/custom_components/sws12500/const.py +++ b/custom_components/sws12500/const.py @@ -63,6 +63,7 @@ OUTSIDE_TEMP: Final = "outside_temp" DEW_POINT: Final = "dew_point" OUTSIDE_HUMIDITY: Final = "outside_humidity" OUTSIDE_CONNECTION: Final = "outside_connection" +OUTSIDE_BATTERY: Final = "outside_battery" WIND_SPEED: Final = "wind_speed" WIND_GUST: Final = "wind_gust" WIND_DIR: Final = "wind_dir" @@ -137,6 +138,7 @@ REMAP_WSLINK_ITEMS: dict = { "t1rainwy": WEEKLY_RAIN, "t1rainmth": MONTHLY_RAIN, "t1rainyr": YEARLY_RAIN, + "t1bat": OUTSIDE_BATTERY, } # TODO: Add more sensors @@ -177,6 +179,26 @@ class UnitOfDir(StrEnum): N = "n" +class UnitOfDir(StrEnum): + """Wind direrction azimut.""" + + NNE = "nne" + NE = "ne" + ENE = "ene" + E = "e" + ESE = "ese" + SE = "se" + SSE = "sse" + S = "s" + SSW = "ssw" + SW = "sw" + WSW = "wsw" + W = "w" + WNW = "wnw" + NW = "nw" + NNW = "nnw" + N = "n" + AZIMUT: list[UnitOfDir] = [ UnitOfDir.NNE, UnitOfDir.NE, @@ -195,3 +217,14 @@ AZIMUT: list[UnitOfDir] = [ UnitOfDir.NNW, UnitOfDir.N, ] + +class UnitOfBat(StrEnum): + """Battery level unit of measure.""" + + LOW = "low" + NORMAL = "normal" + +BATLEVEL: list[UnitOfBat] = [ + UnitOfBat.LOW, + UnitOfBat.NORMAL, +] diff --git a/custom_components/sws12500/sensors_wslink.py b/custom_components/sws12500/sensors_wslink.py index dc09aa9..22aeefe 100644 --- a/custom_components/sws12500/sensors_wslink.py +++ b/custom_components/sws12500/sensors_wslink.py @@ -29,6 +29,7 @@ from .const import ( HEAT_INDEX, INDOOR_HUMIDITY, INDOOR_TEMP, + OUTSIDE_BATTERY, OUTSIDE_HUMIDITY, OUTSIDE_TEMP, RAIN, @@ -45,7 +46,7 @@ from .const import ( WEEKLY_RAIN, ) from .sensors_common import WeatherSensorEntityDescription -from .utils import wind_dir_to_text +from .utils import battery_level_to_text, wind_dir_to_text SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = ( WeatherSensorEntityDescription( @@ -303,4 +304,12 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = ( translation_key=CHILL_INDEX, value_fn=lambda data: cast("int", data), ), + WeatherSensorEntityDescription( + key=OUTSIDE_BATTERY, + name="Outside Battery", + icon="mdi:battery", + device_class=SensorDeviceClass.ENUM, + value_fn=lambda data: cast("str", battery_level_to_text(data)), + translation_key=OUTSIDE_BATTERY, + ), ) diff --git a/custom_components/sws12500/strings.json b/custom_components/sws12500/strings.json index fda0f39..7260ffa 100644 --- a/custom_components/sws12500/strings.json +++ b/custom_components/sws12500/strings.json @@ -135,6 +135,7 @@ } } }, + "outside_battery": { "name": "Outside battery level" }, "notify": { "added": { "title": "New sensors for SWS 12500 found.", diff --git a/custom_components/sws12500/translations/cs.json b/custom_components/sws12500/translations/cs.json index e024ec3..e6009ef 100644 --- a/custom_components/sws12500/translations/cs.json +++ b/custom_components/sws12500/translations/cs.json @@ -139,6 +139,7 @@ } } }, + "outside_battery": { "name": "Vnější úroveň nabití baterie" }, "notify": { "added": { "title": "Nalezeny nové senzory pro SWS 12500.", diff --git a/custom_components/sws12500/translations/en.json b/custom_components/sws12500/translations/en.json index e0ce1a6..1260904 100644 --- a/custom_components/sws12500/translations/en.json +++ b/custom_components/sws12500/translations/en.json @@ -136,6 +136,8 @@ "nw": "NW", "nnw": "NNW" } + }, + "outside_battery": { "name": "Outside battery level" } } } }, diff --git a/custom_components/sws12500/utils.py b/custom_components/sws12500/utils.py index 30a667b..b982910 100644 --- a/custom_components/sws12500/utils.py +++ b/custom_components/sws12500/utils.py @@ -20,6 +20,7 @@ from homeassistant.helpers.translation import async_get_translations from .const import ( AZIMUT, + BATLEVEL, DATABASE_PATH, DEV_DBG, OUTSIDE_HUMIDITY, @@ -180,7 +181,19 @@ def wind_dir_to_text(deg: float) -> UnitOfDir | None: return None +def battery_level_to_text(battery: int) -> str: + """Return battery level in text representation. + Returns UnitOfBat or None + """ + if battery is None: + return "unknown" + + if battery is 0: + return BATLEVEL[battery] + elif battery is 1: + return BATLEVEL[battery] + def fahrenheit_to_celsius(fahrenheit: float) -> float: """Convert Fahrenheit to Celsius.""" return (fahrenheit - 32) * 5.0 / 9.0 From 4d2dedbb11ef527d755c58199823eb99e8c20503 Mon Sep 17 00:00:00 2001 From: FerronN Date: Fri, 11 Jul 2025 10:15:44 +0200 Subject: [PATCH 03/30] Fix structure en.json --- custom_components/sws12500/translations/en.json | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/sws12500/translations/en.json b/custom_components/sws12500/translations/en.json index 1260904..f89b307 100644 --- a/custom_components/sws12500/translations/en.json +++ b/custom_components/sws12500/translations/en.json @@ -138,7 +138,6 @@ } }, "outside_battery": { "name": "Outside battery level" } - } } }, "notify": { From cf0938a6fdb79d228a3ee680c21ff7f484b78191 Mon Sep 17 00:00:00 2001 From: FerronN Date: Fri, 11 Jul 2025 10:16:59 +0200 Subject: [PATCH 04/30] fix data parsing in sensors_wslink.py --- custom_components/sws12500/sensors_wslink.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sws12500/sensors_wslink.py b/custom_components/sws12500/sensors_wslink.py index 22aeefe..50f8b2a 100644 --- a/custom_components/sws12500/sensors_wslink.py +++ b/custom_components/sws12500/sensors_wslink.py @@ -309,7 +309,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = ( name="Outside Battery", icon="mdi:battery", device_class=SensorDeviceClass.ENUM, - value_fn=lambda data: cast("str", battery_level_to_text(data)), + value_fn=lambda data: battery_level_to_text(int(data)) if data is not None and str(data).isdigit() else "unknown", translation_key=OUTSIDE_BATTERY, ), ) From de8d2a7b0c706bdba5c0de6a98e7bd4e9d8bf8a1 Mon Sep 17 00:00:00 2001 From: schizza Date: Sat, 16 Aug 2025 17:29:22 +0200 Subject: [PATCH 05/30] Update const.py --- custom_components/sws12500/const.py | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/custom_components/sws12500/const.py b/custom_components/sws12500/const.py index 28bc375..b790c88 100644 --- a/custom_components/sws12500/const.py +++ b/custom_components/sws12500/const.py @@ -158,27 +158,6 @@ DISABLED_BY_DEFAULT: Final = [ ] -class UnitOfDir(StrEnum): - """Wind direrction azimut.""" - - NNE = "nne" - NE = "ne" - ENE = "ene" - E = "e" - ESE = "ese" - SE = "se" - SSE = "sse" - S = "s" - SSW = "ssw" - SW = "sw" - WSW = "wsw" - W = "w" - WNW = "wnw" - NW = "nw" - NNW = "nnw" - N = "n" - - class UnitOfDir(StrEnum): """Wind direrction azimut.""" @@ -223,8 +202,10 @@ class UnitOfBat(StrEnum): LOW = "low" NORMAL = "normal" + UNKNOWN = "unknown" -BATLEVEL: list[UnitOfBat] = [ +BATTERY_LEVEL: list[UnitOfBat] = [ UnitOfBat.LOW, UnitOfBat.NORMAL, + UnitOfBat.UNKNOWN, ] From 68da7aad9870eafcff9099d55293033bac78b36e Mon Sep 17 00:00:00 2001 From: schizza Date: Sun, 17 Aug 2025 18:33:42 +0200 Subject: [PATCH 06/30] Improves battery level representation Refactors battery level representation by using enum instead of string. Improves battery level display by adding an icon representation. Changes const BATLEVEL to BATTERY_LEVEL. --- custom_components/sws12500/utils.py | 45 ++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/custom_components/sws12500/utils.py b/custom_components/sws12500/utils.py index b982910..c78f63f 100644 --- a/custom_components/sws12500/utils.py +++ b/custom_components/sws12500/utils.py @@ -20,7 +20,7 @@ from homeassistant.helpers.translation import async_get_translations from .const import ( AZIMUT, - BATLEVEL, + BATTERY_LEVEL, DATABASE_PATH, DEV_DBG, OUTSIDE_HUMIDITY, @@ -30,6 +30,7 @@ from .const import ( SENSORS_TO_LOAD, WIND_SPEED, UnitOfDir, + UnitOfBat, ) _LOGGER = logging.getLogger(__name__) @@ -181,19 +182,33 @@ def wind_dir_to_text(deg: float) -> UnitOfDir | None: return None -def battery_level_to_text(battery: int) -> str: + +def battery_level_to_text(battery: int) -> UnitOfBat: """Return battery level in text representation. - Returns UnitOfBat or None + Returns UnitOfBat """ - if battery is None: - return "unknown" - if battery is 0: - return BATLEVEL[battery] - elif battery is 1: - return BATLEVEL[battery] - + return { + 0: UnitOfBat.LOW, + 1: UnitOfBat.NORMAL, + }.get(battery, UnitOfBat.UNKNOWN) + + +def battery_level_to_icon(battery: UnitOfBat) -> str: + """Return battery level in icon representation. + + Returns str + """ + + icons = { + UnitOfBat.LOW: "mdi:battery-alert", + UnitOfBat.NORMAL: "mdi:battery", + } + + return icons.get(battery, "mdi:battery-unknown") + + def fahrenheit_to_celsius(fahrenheit: float) -> float: """Convert Fahrenheit to Celsius.""" return (fahrenheit - 32) * 5.0 / 9.0 @@ -280,10 +295,12 @@ def long_term_units_in_statistics_meta(): db = conn.cursor() try: - db.execute(""" + db.execute( + """ SELECT statistic_id, unit_of_measurement from statistics_meta WHERE statistic_id LIKE 'sensor.weather_station_sws%' - """) + """ + ) rows = db.fetchall() sensor_units = { statistic_id: f"{statistic_id} ({unit})" for statistic_id, unit in rows @@ -299,8 +316,8 @@ def long_term_units_in_statistics_meta(): async def migrate_data(hass: HomeAssistant, sensor_id: str | None = None) -> bool: """Migrate data from mm/d to mm.""" - - _LOGGER.debug("Sensor %s is required for data migration", sensor_id) + + _LOGGER.debug("Sensor %s is required for data migration", sensor_id) updated_rows = 0 if not Path(DATABASE_PATH).exists(): From bbe31da4c5dc614edc0b72fc065cfaa8a0845c22 Mon Sep 17 00:00:00 2001 From: schizza Date: Mon, 18 Aug 2025 10:55:37 +0200 Subject: [PATCH 07/30] Adds battery state translations Adds translations for the battery state of the outside sensor to both the English and Czech language files. This change provides more descriptive and user-friendly information about the battery status. --- custom_components/sws12500/strings.json | 9 ++++++++- custom_components/sws12500/translations/cs.json | 9 ++++++++- custom_components/sws12500/translations/en.json | 9 ++++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/custom_components/sws12500/strings.json b/custom_components/sws12500/strings.json index 7260ffa..3472af0 100644 --- a/custom_components/sws12500/strings.json +++ b/custom_components/sws12500/strings.json @@ -131,11 +131,18 @@ "wnw": "WNW", "nw": "NW", "nnw": "NNW" + }, + "outside_battery": { + "name": "Outside battery level", + "state": { + "normal": "OK", + "low": "Low", + "unknown": "Unknown / drained out" + } } } } }, - "outside_battery": { "name": "Outside battery level" }, "notify": { "added": { "title": "New sensors for SWS 12500 found.", diff --git a/custom_components/sws12500/translations/cs.json b/custom_components/sws12500/translations/cs.json index e6009ef..8da9943 100644 --- a/custom_components/sws12500/translations/cs.json +++ b/custom_components/sws12500/translations/cs.json @@ -136,10 +136,17 @@ "nw": "SZ", "nnw": "SSZ" } + }, + "outside_battery": { + "name": "Stav nabití venkovní baterie", + "state": { + "low": "Nízká", + "normal": "Normální", + "unknown": "Neznámá / zcela vybitá" + } } } }, - "outside_battery": { "name": "Vnější úroveň nabití baterie" }, "notify": { "added": { "title": "Nalezeny nové senzory pro SWS 12500.", diff --git a/custom_components/sws12500/translations/en.json b/custom_components/sws12500/translations/en.json index f89b307..b0f3567 100644 --- a/custom_components/sws12500/translations/en.json +++ b/custom_components/sws12500/translations/en.json @@ -137,7 +137,14 @@ "nnw": "NNW" } }, - "outside_battery": { "name": "Outside battery level" } + "outside_battery": { + "name": "Outside battery level", + "state": { + "normal": "OK", + "low": "Low", + "unknown": "Unknown / drained out" + } + } } }, "notify": { From 09d79e20320ce3ebc05aa6730d3a836ff7c11893 Mon Sep 17 00:00:00 2001 From: schizza Date: Mon, 18 Aug 2025 12:50:31 +0200 Subject: [PATCH 08/30] Improves battery sensor display Updates the outside battery sensor to display an icon reflecting the battery level, enhancing the user experience by providing a visual indication of the battery status. --- custom_components/sws12500/sensors_wslink.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/custom_components/sws12500/sensors_wslink.py b/custom_components/sws12500/sensors_wslink.py index 50f8b2a..9144d83 100644 --- a/custom_components/sws12500/sensors_wslink.py +++ b/custom_components/sws12500/sensors_wslink.py @@ -46,7 +46,7 @@ from .const import ( WEEKLY_RAIN, ) from .sensors_common import WeatherSensorEntityDescription -from .utils import battery_level_to_text, wind_dir_to_text +from .utils import battery_level_to_icon, battery_level_to_text, wind_dir_to_text SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = ( WeatherSensorEntityDescription( @@ -306,10 +306,9 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = ( ), WeatherSensorEntityDescription( key=OUTSIDE_BATTERY, - name="Outside Battery", - icon="mdi:battery", - device_class=SensorDeviceClass.ENUM, - value_fn=lambda data: battery_level_to_text(int(data)) if data is not None and str(data).isdigit() else "unknown", translation_key=OUTSIDE_BATTERY, + icon=lambda data: battery_level_to_icon(battery_level_to_text(int(data))), + device_class=SensorDeviceClass.ENUM, + value_fn=lambda data: battery_level_to_text(int(data)), ), ) From 1ecd88269d5483f13e66957f9a1ce72cd3e4e6c4 Mon Sep 17 00:00:00 2001 From: schizza Date: Mon, 18 Aug 2025 12:53:40 +0200 Subject: [PATCH 09/30] Adds missing constant and improves readability Adds OUTSIDE_BATTERY to the DISABLED_BY_DEFAULT list. Improves readability by formatting long strings with parenthesis. --- custom_components/sws12500/const.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/custom_components/sws12500/const.py b/custom_components/sws12500/const.py index b790c88..f5b41e7 100644 --- a/custom_components/sws12500/const.py +++ b/custom_components/sws12500/const.py @@ -23,8 +23,12 @@ WSLINK: Final = "wslink" WINDY_API_KEY = "WINDY_API_KEY" WINDY_ENABLED: Final = "windy_enabled_checkbox" WINDY_LOGGER_ENABLED: Final = "windy_logger_checkbox" -WINDY_NOT_INSERTED: Final = "Data was succefuly sent to Windy, but not inserted by Windy API. Does anyone else sent data to Windy?" -WINDY_INVALID_KEY: Final = "Windy API KEY is invalid. Send data to Windy is now disabled. Check your API KEY and try again." +WINDY_NOT_INSERTED: Final = ( + "Data was succefuly sent to Windy, but not inserted by Windy API. Does anyone else sent data to Windy?" +) +WINDY_INVALID_KEY: Final = ( + "Windy API KEY is invalid. Send data to Windy is now disabled. Check your API KEY and try again." +) WINDY_SUCCESS: Final = ( "Windy successfully sent data and data was successfully inserted by Windy API" ) @@ -155,6 +159,7 @@ DISABLED_BY_DEFAULT: Final = [ CH3_HUMIDITY, CH4_TEMP, CH4_HUMIDITY, + OUTSIDE_BATTERY, ] @@ -178,6 +183,7 @@ class UnitOfDir(StrEnum): NNW = "nnw" N = "n" + AZIMUT: list[UnitOfDir] = [ UnitOfDir.NNE, UnitOfDir.NE, @@ -197,6 +203,7 @@ AZIMUT: list[UnitOfDir] = [ UnitOfDir.N, ] + class UnitOfBat(StrEnum): """Battery level unit of measure.""" @@ -204,6 +211,7 @@ class UnitOfBat(StrEnum): NORMAL = "normal" UNKNOWN = "unknown" + BATTERY_LEVEL: list[UnitOfBat] = [ UnitOfBat.LOW, UnitOfBat.NORMAL, From e11e068c0f6596f63fdf8da7066b87e3273610ce Mon Sep 17 00:00:00 2001 From: schizza Date: Mon, 18 Aug 2025 13:29:00 +0200 Subject: [PATCH 10/30] Reorders constants for better readability Reorders the import of constants to improve readability and maintain consistency within the module. Final touches. --- custom_components/sws12500/sensors_wslink.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/sws12500/sensors_wslink.py b/custom_components/sws12500/sensors_wslink.py index 9144d83..d808564 100644 --- a/custom_components/sws12500/sensors_wslink.py +++ b/custom_components/sws12500/sensors_wslink.py @@ -27,23 +27,23 @@ from .const import ( DAILY_RAIN, DEW_POINT, HEAT_INDEX, + HOURLY_RAIN, INDOOR_HUMIDITY, INDOOR_TEMP, + MONTHLY_RAIN, OUTSIDE_BATTERY, OUTSIDE_HUMIDITY, OUTSIDE_TEMP, RAIN, SOLAR_RADIATION, UV, + WEEKLY_RAIN, WIND_AZIMUT, WIND_DIR, WIND_GUST, WIND_SPEED, - UnitOfDir, - MONTHLY_RAIN, YEARLY_RAIN, - HOURLY_RAIN, - WEEKLY_RAIN, + UnitOfDir, ) from .sensors_common import WeatherSensorEntityDescription from .utils import battery_level_to_icon, battery_level_to_text, wind_dir_to_text From 0027a809686719a73b1a2ac10703dee90664d68e Mon Sep 17 00:00:00 2001 From: schizza Date: Thu, 21 Aug 2025 16:08:08 +0200 Subject: [PATCH 11/30] Update const to stable version Update constants to stable version. --- custom_components/sws12500/const.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/sws12500/const.py b/custom_components/sws12500/const.py index f5b41e7..f0faff9 100644 --- a/custom_components/sws12500/const.py +++ b/custom_components/sws12500/const.py @@ -142,7 +142,8 @@ REMAP_WSLINK_ITEMS: dict = { "t1rainwy": WEEKLY_RAIN, "t1rainmth": MONTHLY_RAIN, "t1rainyr": YEARLY_RAIN, - "t1bat": OUTSIDE_BATTERY, + "t234c2tem": CH3_TEMP, + "t234c2hum": CH3_HUMIDITY, } # TODO: Add more sensors From 2d758835dc759bccc84fd5a20c580699183e0376 Mon Sep 17 00:00:00 2001 From: schizza Date: Thu, 21 Aug 2025 16:30:10 +0200 Subject: [PATCH 12/30] config_flow migrated to stable version. Config flow was migrated to stable version. Removes the unit migration flow, which is intended to introduce later. --- custom_components/sws12500/config_flow.py | 192 +--------------------- 1 file changed, 2 insertions(+), 190 deletions(-) diff --git a/custom_components/sws12500/config_flow.py b/custom_components/sws12500/config_flow.py index 9e6b2b2..3b693da 100644 --- a/custom_components/sws12500/config_flow.py +++ b/custom_components/sws12500/config_flow.py @@ -1,15 +1,13 @@ """Config flow for Sencor SWS 12500 Weather Station integration.""" -import logging from typing import Any import voluptuous as vol from homeassistant.config_entries import ConfigFlow, OptionsFlow -from homeassistant.const import UnitOfPrecipitationDepth, UnitOfVolumetricFlux from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.entity_registry as er +from .utils import long_term_units_in_statistics_meta, migrate_data from .const import ( API_ID, @@ -17,18 +15,13 @@ from .const import ( DEV_DBG, DOMAIN, INVALID_CREDENTIALS, - MIG_FROM, - MIG_TO, - SENSOR_TO_MIGRATE, SENSORS_TO_LOAD, + SENSOR_TO_MIGRATE, WINDY_API_KEY, WINDY_ENABLED, WINDY_LOGGER_ENABLED, WSLINK, ) -from .utils import long_term_units_in_statistics_meta, migrate_data - -_LOGGER = logging.getLogger(__name__) class CannotConnect(HomeAssistantError): @@ -52,13 +45,6 @@ class ConfigOptionsFlowHandler(OptionsFlow): self.user_data_schema = {} self.sensors: dict[str, Any] = {} self.migrate_schema = {} - self.migrate_sensor_select = {} - self.migrate_unit_selection = {} - self.count = 0 - self.selected_sensor = "" - - self.unit_values = [unit.value for unit in UnitOfVolumetricFlux] - self.unit_values.extend([unit.value for unit in UnitOfPrecipitationDepth]) @property def config_entry(self): @@ -107,35 +93,6 @@ class ConfigOptionsFlowHandler(OptionsFlow): ): bool or False, } - self.migrate_sensor_select = { - vol.Required(SENSOR_TO_MIGRATE): vol.In( - await self.load_sensors_to_migrate() or {} - ), - } - - self.migrate_unit_selection = { - vol.Required(MIG_FROM): vol.In(self.unit_values), - vol.Required(MIG_TO): vol.In(self.unit_values), - vol.Optional("trigger_action", default=False): bool, - } - # "mm/d", "mm/h", "mm", "in/d", "in/h", "in" - - async def load_sensors_to_migrate(self) -> dict[str, Any]: - """Load sensors to migrate.""" - - sensor_statistics = await long_term_units_in_statistics_meta(self.hass) - - entity_registry = er.async_get(self.hass) - sensors = entity_registry.entities.get_entries_for_config_entry_id( - self.config_entry.entry_id - ) - - return { - sensor.entity_id: f"{sensor.name or sensor.original_name} (current settings: {sensor.unit_of_measurement}, longterm stats unit: {sensor_statistics.get(sensor.entity_id)})" - for sensor in sensors - if sensor.unique_id in {"rain", "daily_rain"} - } - async def async_step_init(self, user_input=None): """Manage the options - show menu first.""" return self.async_show_menu( @@ -208,151 +165,6 @@ class ConfigOptionsFlowHandler(OptionsFlow): return self.async_create_entry(title=DOMAIN, data=user_input) - async def async_step_migration(self, user_input=None): - """Migrate sensors.""" - - errors = {} - - data_schema = vol.Schema(self.migrate_sensor_select) - data_schema.schema.update() - - await self._get_entry_data() - - if user_input is None: - return self.async_show_form( - step_id="migration", - data_schema=vol.Schema(self.migrate_sensor_select), - errors=errors, - description_placeholders={ - "migration_status": "-", - "migration_count": "-", - }, - ) - - self.selected_sensor = user_input.get(SENSOR_TO_MIGRATE) - - return await self.async_step_migration_units() - - async def async_step_migration_units(self, user_input=None): - """Migrate unit step.""" - - registry = er.async_get(self.hass) - sensor_entry = registry.async_get(self.selected_sensor) - sensor_stats = await long_term_units_in_statistics_meta(self.hass) - - default_unit = sensor_entry.unit_of_measurement if sensor_entry else None - - if default_unit not in self.unit_values: - default_unit = self.unit_values[0] - - data_schema = vol.Schema({ - vol.Required(MIG_FROM, default=default_unit): vol.In(self.unit_values), - vol.Required(MIG_TO): vol.In(self.unit_values), - vol.Optional("trigger_action", default=False): bool, - }) - - if user_input is None: - return self.async_show_form( - step_id="migration_units", - data_schema=data_schema, - errors={}, - description_placeholders={ - "migration_sensor": sensor_entry.original_name, - "migration_stats": sensor_stats.get(self.selected_sensor), - }, - ) - - if user_input.get("trigger_action"): - self.count = await migrate_data( - self.hass, - self.selected_sensor, - user_input.get(MIG_FROM), - user_input.get(MIG_TO), - ) - - registry.async_update_entity(self.selected_sensor, - unit_of_measurement=user_input.get(MIG_TO), - ) - - state = self.hass.states.get(self.selected_sensor) - if state: - _LOGGER.info("State attributes before update: %s", state.attributes) - attributes = dict(state.attributes) - attributes["unit_of_measurement"] = user_input.get(MIG_TO) - self.hass.states.async_set(self.selected_sensor, state.state, attributes) - _LOGGER.info("State attributes after update: %s", attributes) - - options = {**self.config_entry.options, "reload_sensor": self.selected_sensor} - self.hass.config_entries.async_update_entry(self.config_entry, options=options) - - await self.hass.config_entries.async_reload(self.config_entry.entry_id) - - await self.hass.async_block_till_done() - - _LOGGER.info("Migration complete for sensor %s: %s row updated, new measurement unit: %s, ", - self.selected_sensor, - self.count, - user_input.get(MIG_TO), - ) - - await self._get_entry_data() - sensor_entry = er.async_get(self.hass).async_get(self.selected_sensor) - sensor_stat = await self.load_sensors_to_migrate() - - return self.async_show_form( - step_id="migration_complete", - data_schema=vol.Schema({}), - errors={}, - description_placeholders={ - "migration_sensor": sensor_entry.unit_of_measurement, - "migration_stats": sensor_stat.get(self.selected_sensor), - "migration_count": self.count, - }, - ) - - # retain windy data - user_input.update(self.windy_data) - - # retain user_data - user_input.update(self.user_data) - - # retain senors - user_input.update(self.sensors) - - return self.async_create_entry(title=DOMAIN, data=user_input) - - async def async_step_migration_complete(self, user_input=None): - """Migration complete.""" - - errors = {} - - await self._get_entry_data() - sensor_entry = er.async_get(self.hass).async_get(self.selected_sensor) - sensor_stat = await self.load_sensors_to_migrate() - - if user_input is None: - return self.async_show_form( - step_id="migration_complete", - data_schema=vol.Schema({}), - errors=errors, - description_placeholders={ - "migration_sensor": sensor_entry.unit_of_measurement, - "migration_stats": sensor_stat.get(self.selected_sensor), - "migration_count": self.count, - }, - ) - - # retain windy data - user_input.update(self.windy_data) - - # retain user_data - user_input.update(self.user_data) - - # retain senors - user_input.update(self.sensors) - - return self.async_create_entry(title=DOMAIN, data=user_input) - class ConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Sencor SWS 12500 Weather Station.""" From 827fb71e25ae29c750e8950c9d03a95ba5a0feb4 Mon Sep 17 00:00:00 2001 From: schizza Date: Thu, 21 Aug 2025 16:39:05 +0200 Subject: [PATCH 13/30] sensors_wslink updated to stable version Updating to stable version, retaining CH3 sensors. Left outside battery unchanged. Will work on bug in next commit. --- custom_components/sws12500/sensors_wslink.py | 38 ++++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/custom_components/sws12500/sensors_wslink.py b/custom_components/sws12500/sensors_wslink.py index d808564..0ef59ac 100644 --- a/custom_components/sws12500/sensors_wslink.py +++ b/custom_components/sws12500/sensors_wslink.py @@ -244,25 +244,25 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = ( translation_key=CH2_HUMIDITY, value_fn=lambda data: cast("int", data), ), - # WeatherSensorEntityDescription( - # key=CH3_TEMP, - # native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, - # state_class=SensorStateClass.MEASUREMENT, - # device_class=SensorDeviceClass.TEMPERATURE, - # suggested_unit_of_measurement=UnitOfTemperature.CELSIUS, - # icon="mdi:weather-sunny", - # translation_key=CH3_TEMP, - # value_fn=lambda data: cast(float, data), - # ), - # WeatherSensorEntityDescription( - # key=CH3_HUMIDITY, - # native_unit_of_measurement=PERCENTAGE, - # state_class=SensorStateClass.MEASUREMENT, - # device_class=SensorDeviceClass.HUMIDITY, - # icon="mdi:weather-sunny", - # translation_key=CH3_HUMIDITY, - # value_fn=lambda data: cast(int, data), - # ), + WeatherSensorEntityDescription( + key=CH3_TEMP, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + suggested_unit_of_measurement=UnitOfTemperature.CELSIUS, + icon="mdi:weather-sunny", + translation_key=CH3_TEMP, + value_fn=lambda data: cast(float, data), + ), + WeatherSensorEntityDescription( + key=CH3_HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + icon="mdi:weather-sunny", + translation_key=CH3_HUMIDITY, + value_fn=lambda data: cast(int, data), + ), # WeatherSensorEntityDescription( # key=CH4_TEMP, # native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, From d4d2440ae80fea4c78f80320acd547330563d1f8 Mon Sep 17 00:00:00 2001 From: schizza Date: Thu, 21 Aug 2025 16:45:29 +0200 Subject: [PATCH 14/30] Removes unused import from config_flow Removes the unused import of `utils` to improve code cleanliness and avoid potential namespace conflicts. Removed 'migration' from menu as it is intended to use in later version. --- custom_components/sws12500/config_flow.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/sws12500/config_flow.py b/custom_components/sws12500/config_flow.py index 3b693da..342589c 100644 --- a/custom_components/sws12500/config_flow.py +++ b/custom_components/sws12500/config_flow.py @@ -7,7 +7,6 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow, OptionsFlow from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from .utils import long_term_units_in_statistics_meta, migrate_data from .const import ( API_ID, From fc8349c06eb18658daccf7d832542898917ac639 Mon Sep 17 00:00:00 2001 From: schizza Date: Thu, 21 Aug 2025 16:46:13 +0200 Subject: [PATCH 15/30] Option flow configuration Removes the "migration" step from the option flow menu. This step will be used in next release. --- custom_components/sws12500/config_flow.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/custom_components/sws12500/config_flow.py b/custom_components/sws12500/config_flow.py index 342589c..0717ccf 100644 --- a/custom_components/sws12500/config_flow.py +++ b/custom_components/sws12500/config_flow.py @@ -15,7 +15,6 @@ from .const import ( DOMAIN, INVALID_CREDENTIALS, SENSORS_TO_LOAD, - SENSOR_TO_MIGRATE, WINDY_API_KEY, WINDY_ENABLED, WINDY_LOGGER_ENABLED, @@ -67,9 +66,11 @@ class ConfigOptionsFlowHandler(OptionsFlow): } self.sensors: dict[str, Any] = { - SENSORS_TO_LOAD: self.config_entry.options.get(SENSORS_TO_LOAD) - if isinstance(self.config_entry.options.get(SENSORS_TO_LOAD), list) - else [] + SENSORS_TO_LOAD: ( + self.config_entry.options.get(SENSORS_TO_LOAD) + if isinstance(self.config_entry.options.get(SENSORS_TO_LOAD), list) + else [] + ) } self.windy_data: dict[str, Any] = { @@ -89,14 +90,13 @@ class ConfigOptionsFlowHandler(OptionsFlow): vol.Optional( WINDY_LOGGER_ENABLED, default=self.windy_data[WINDY_LOGGER_ENABLED], - ): bool or False, + ): bool + or False, } async def async_step_init(self, user_input=None): """Manage the options - show menu first.""" - return self.async_show_menu( - step_id="init", menu_options=["basic", "windy", "migration"] - ) + return self.async_show_menu(step_id="init", menu_options=["basic", "windy"]) async def async_step_basic(self, user_input=None): """Manage basic options - credentials.""" From e10ea9901cc93500dd8af943b4fc4ce58a8a01a1 Mon Sep 17 00:00:00 2001 From: schizza Date: Fri, 22 Aug 2025 18:06:35 +0200 Subject: [PATCH 16/30] Adds outside battery sensor Adds an outside battery sensor to the integration, providing information about the battery level of the outdoor sensor. This includes: - Mapping the `t1bat` WSLink item to the `OUTSIDE_BATTERY` sensor. - Implementing logic to convert the battery level to a human-readable text representation and a corresponding icon. - Updates precipitation to intensity and fixes data type of battery level --- custom_components/sws12500/const.py | 1 + custom_components/sws12500/sensor.py | 19 +++++++++++++++++-- custom_components/sws12500/sensors_wslink.py | 6 +++--- custom_components/sws12500/utils.py | 4 ++-- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/custom_components/sws12500/const.py b/custom_components/sws12500/const.py index f0faff9..afe1979 100644 --- a/custom_components/sws12500/const.py +++ b/custom_components/sws12500/const.py @@ -144,6 +144,7 @@ REMAP_WSLINK_ITEMS: dict = { "t1rainyr": YEARLY_RAIN, "t234c2tem": CH3_TEMP, "t234c2hum": CH3_HUMIDITY, + "t1bat": OUTSIDE_BATTERY, } # TODO: Add more sensors diff --git a/custom_components/sws12500/sensor.py b/custom_components/sws12500/sensor.py index a0275a2..0be5eff 100644 --- a/custom_components/sws12500/sensor.py +++ b/custom_components/sws12500/sensor.py @@ -15,6 +15,7 @@ from .const import ( CHILL_INDEX, DOMAIN, HEAT_INDEX, + OUTSIDE_BATTERY, OUTSIDE_HUMIDITY, OUTSIDE_TEMP, SENSORS_TO_LOAD, @@ -26,7 +27,7 @@ from .const import ( from .sensors_common import WeatherSensorEntityDescription from .sensors_weather import SENSOR_TYPES_WEATHER_API from .sensors_wslink import SENSOR_TYPES_WSLINK -from .utils import chill_index, heat_index +from .utils import chill_index, heat_index, battery_level_to_icon, battery_level_to_text _LOGGER = logging.getLogger(__name__) @@ -130,13 +131,27 @@ class WeatherSensor( ): return self.entity_description.value_fn(chill_index(self.coordinator.data)) - return None if self._data == "" else self.entity_description.value_fn(self._data) + return ( + None if self._data == "" else self.entity_description.value_fn(self._data) + ) @property def suggested_entity_id(self) -> str: """Return name.""" return generate_entity_id("sensor.{}", self.entity_description.key) + @property + def icon(self) -> str | None: + """Return the dynamic icon for battery representation.""" + + if self.entity_description.key == OUTSIDE_BATTERY: + try: + return battery_level_to_icon(self.native_value) + except Exception: + return "mdi:battery-unknown" + + return self.entity_description.icon + @property def device_info(self) -> DeviceInfo: """Device info.""" diff --git a/custom_components/sws12500/sensors_wslink.py b/custom_components/sws12500/sensors_wslink.py index 0ef59ac..f0d3c7a 100644 --- a/custom_components/sws12500/sensors_wslink.py +++ b/custom_components/sws12500/sensors_wslink.py @@ -144,7 +144,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = ( WeatherSensorEntityDescription( key=RAIN, native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, - device_class=SensorDeviceClass.PRECIPITATION, + device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, state_class=SensorStateClass.TOTAL, suggested_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, suggested_display_precision=2, @@ -307,8 +307,8 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = ( WeatherSensorEntityDescription( key=OUTSIDE_BATTERY, translation_key=OUTSIDE_BATTERY, - icon=lambda data: battery_level_to_icon(battery_level_to_text(int(data))), + icon="mdi:battery-unknown", device_class=SensorDeviceClass.ENUM, - value_fn=lambda data: battery_level_to_text(int(data)), + value_fn=lambda data: battery_level_to_text(data), ), ) diff --git a/custom_components/sws12500/utils.py b/custom_components/sws12500/utils.py index c78f63f..52fe68f 100644 --- a/custom_components/sws12500/utils.py +++ b/custom_components/sws12500/utils.py @@ -192,7 +192,7 @@ def battery_level_to_text(battery: int) -> UnitOfBat: return { 0: UnitOfBat.LOW, 1: UnitOfBat.NORMAL, - }.get(battery, UnitOfBat.UNKNOWN) + }.get(int(battery) if battery is not None else None, UnitOfBat.UNKNOWN) def battery_level_to_icon(battery: UnitOfBat) -> str: @@ -202,7 +202,7 @@ def battery_level_to_icon(battery: UnitOfBat) -> str: """ icons = { - UnitOfBat.LOW: "mdi:battery-alert", + UnitOfBat.LOW: "mdi:battery-low", UnitOfBat.NORMAL: "mdi:battery", } From f14e6500d447bf5470a5f32ded71744b541935b3 Mon Sep 17 00:00:00 2001 From: schizza Date: Thu, 24 Apr 2025 17:56:47 +0200 Subject: [PATCH 17/30] Adds unit migration functionality to options flow Implements a user interface to migrate units for rain sensors including migration of historic data via statistics. This provides the user with the ability to correct rain units, if they have been set incorrectly. Includes UI to select sensor and units, as well as trigger migration. --- custom_components/sws12500/config_flow.py | 188 +++++++++++++++++++--- 1 file changed, 162 insertions(+), 26 deletions(-) diff --git a/custom_components/sws12500/config_flow.py b/custom_components/sws12500/config_flow.py index 508d01e..9e6b2b2 100644 --- a/custom_components/sws12500/config_flow.py +++ b/custom_components/sws12500/config_flow.py @@ -1,13 +1,15 @@ """Config flow for Sencor SWS 12500 Weather Station integration.""" +import logging from typing import Any import voluptuous as vol from homeassistant.config_entries import ConfigFlow, OptionsFlow +from homeassistant.const import UnitOfPrecipitationDepth, UnitOfVolumetricFlux from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from .utils import long_term_units_in_statistics_meta, migrate_data +import homeassistant.helpers.entity_registry as er from .const import ( API_ID, @@ -15,13 +17,18 @@ from .const import ( DEV_DBG, DOMAIN, INVALID_CREDENTIALS, - SENSORS_TO_LOAD, + MIG_FROM, + MIG_TO, SENSOR_TO_MIGRATE, + SENSORS_TO_LOAD, WINDY_API_KEY, WINDY_ENABLED, WINDY_LOGGER_ENABLED, WSLINK, ) +from .utils import long_term_units_in_statistics_meta, migrate_data + +_LOGGER = logging.getLogger(__name__) class CannotConnect(HomeAssistantError): @@ -41,16 +48,23 @@ class ConfigOptionsFlowHandler(OptionsFlow): self.windy_data: dict[str, Any] = {} self.windy_data_schema = {} - self.user_data: dict[str, str] = {} + self.user_data: dict[str, Any] = {} self.user_data_schema = {} self.sensors: dict[str, Any] = {} self.migrate_schema = {} + self.migrate_sensor_select = {} + self.migrate_unit_selection = {} + self.count = 0 + self.selected_sensor = "" + + self.unit_values = [unit.value for unit in UnitOfVolumetricFlux] + self.unit_values.extend([unit.value for unit in UnitOfPrecipitationDepth]) @property def config_entry(self): return self.hass.config_entries.async_get_entry(self.handler) - def _get_entry_data(self): + async def _get_entry_data(self): """Get entry data.""" self.user_data: dict[str, Any] = { @@ -61,10 +75,10 @@ class ConfigOptionsFlowHandler(OptionsFlow): } self.user_data_schema = { - vol.Required(API_ID, default=self.user_data[API_ID] or ""): str, - vol.Required(API_KEY, default=self.user_data[API_KEY] or ""): str, - vol.Optional(WSLINK, default=self.user_data[WSLINK]): bool or False, - vol.Optional(DEV_DBG, default=self.user_data[DEV_DBG]): bool or False, + vol.Required(API_ID, default=self.user_data.get(API_ID, "")): str, + vol.Required(API_KEY, default=self.user_data.get(API_KEY, "")): str, + vol.Optional(WSLINK, default=self.user_data.get(WSLINK, False)): bool, + vol.Optional(DEV_DBG, default=self.user_data.get(DEV_DBG, False)): bool, } self.sensors: dict[str, Any] = { @@ -83,7 +97,7 @@ class ConfigOptionsFlowHandler(OptionsFlow): self.windy_data_schema = { vol.Optional( - WINDY_API_KEY, default=self.windy_data[WINDY_API_KEY] or "" + WINDY_API_KEY, default=self.windy_data.get(WINDY_API_KEY, "") ): str, vol.Optional(WINDY_ENABLED, default=self.windy_data[WINDY_ENABLED]): bool or False, @@ -93,12 +107,34 @@ class ConfigOptionsFlowHandler(OptionsFlow): ): bool or False, } - self.migrate_schema = { + self.migrate_sensor_select = { vol.Required(SENSOR_TO_MIGRATE): vol.In( - long_term_units_in_statistics_meta() or {} + await self.load_sensors_to_migrate() or {} ), + } + + self.migrate_unit_selection = { + vol.Required(MIG_FROM): vol.In(self.unit_values), + vol.Required(MIG_TO): vol.In(self.unit_values), vol.Optional("trigger_action", default=False): bool, } + # "mm/d", "mm/h", "mm", "in/d", "in/h", "in" + + async def load_sensors_to_migrate(self) -> dict[str, Any]: + """Load sensors to migrate.""" + + sensor_statistics = await long_term_units_in_statistics_meta(self.hass) + + entity_registry = er.async_get(self.hass) + sensors = entity_registry.entities.get_entries_for_config_entry_id( + self.config_entry.entry_id + ) + + return { + sensor.entity_id: f"{sensor.name or sensor.original_name} (current settings: {sensor.unit_of_measurement}, longterm stats unit: {sensor_statistics.get(sensor.entity_id)})" + for sensor in sensors + if sensor.unique_id in {"rain", "daily_rain"} + } async def async_step_init(self, user_input=None): """Manage the options - show menu first.""" @@ -110,7 +146,7 @@ class ConfigOptionsFlowHandler(OptionsFlow): """Manage basic options - credentials.""" errors = {} - self._get_entry_data() + await self._get_entry_data() if user_input is None: return self.async_show_form( @@ -147,7 +183,7 @@ class ConfigOptionsFlowHandler(OptionsFlow): """Manage windy options.""" errors = {} - self._get_entry_data() + await self._get_entry_data() if user_input is None: return self.async_show_form( @@ -160,7 +196,7 @@ class ConfigOptionsFlowHandler(OptionsFlow): errors[WINDY_API_KEY] = "windy_key_required" return self.async_show_form( step_id="windy", - data_schema=self.windy_data_schema, + data_schema=vol.Schema(self.windy_data_schema), errors=errors, ) @@ -175,15 +211,17 @@ class ConfigOptionsFlowHandler(OptionsFlow): async def async_step_migration(self, user_input=None): """Migrate sensors.""" - # hj errors = {} - self._get_entry_data() + data_schema = vol.Schema(self.migrate_sensor_select) + data_schema.schema.update() + + await self._get_entry_data() if user_input is None: return self.async_show_form( step_id="migration", - data_schema=vol.Schema(self.migrate_schema), + data_schema=vol.Schema(self.migrate_sensor_select), errors=errors, description_placeholders={ "migration_status": "-", @@ -191,18 +229,116 @@ class ConfigOptionsFlowHandler(OptionsFlow): }, ) - if user_input.get("trigger_action"): - # Akce se vykoná po zaškrtnutí - count = await self.hass.async_add_executor_job( - migrate_data, user_input.get(SENSOR_TO_MIGRATE) - ) + self.selected_sensor = user_input.get(SENSOR_TO_MIGRATE) + + return await self.async_step_migration_units() + + async def async_step_migration_units(self, user_input=None): + """Migrate unit step.""" + + registry = er.async_get(self.hass) + sensor_entry = registry.async_get(self.selected_sensor) + sensor_stats = await long_term_units_in_statistics_meta(self.hass) + + default_unit = sensor_entry.unit_of_measurement if sensor_entry else None + + if default_unit not in self.unit_values: + default_unit = self.unit_values[0] + + data_schema = vol.Schema({ + vol.Required(MIG_FROM, default=default_unit): vol.In(self.unit_values), + vol.Required(MIG_TO): vol.In(self.unit_values), + vol.Optional("trigger_action", default=False): bool, + }) + + if user_input is None: return self.async_show_form( - step_id="migration", - data_schema=vol.Schema(self.migrate_schema), + step_id="migration_units", + data_schema=data_schema, + errors={}, + description_placeholders={ + "migration_sensor": sensor_entry.original_name, + "migration_stats": sensor_stats.get(self.selected_sensor), + }, + ) + + if user_input.get("trigger_action"): + self.count = await migrate_data( + self.hass, + self.selected_sensor, + user_input.get(MIG_FROM), + user_input.get(MIG_TO), + ) + + registry.async_update_entity(self.selected_sensor, + unit_of_measurement=user_input.get(MIG_TO), + ) + + state = self.hass.states.get(self.selected_sensor) + if state: + _LOGGER.info("State attributes before update: %s", state.attributes) + attributes = dict(state.attributes) + attributes["unit_of_measurement"] = user_input.get(MIG_TO) + self.hass.states.async_set(self.selected_sensor, state.state, attributes) + _LOGGER.info("State attributes after update: %s", attributes) + + options = {**self.config_entry.options, "reload_sensor": self.selected_sensor} + self.hass.config_entries.async_update_entry(self.config_entry, options=options) + + await self.hass.config_entries.async_reload(self.config_entry.entry_id) + + await self.hass.async_block_till_done() + + _LOGGER.info("Migration complete for sensor %s: %s row updated, new measurement unit: %s, ", + self.selected_sensor, + self.count, + user_input.get(MIG_TO), + ) + + await self._get_entry_data() + sensor_entry = er.async_get(self.hass).async_get(self.selected_sensor) + sensor_stat = await self.load_sensors_to_migrate() + + return self.async_show_form( + step_id="migration_complete", + data_schema=vol.Schema({}), + errors={}, + description_placeholders={ + "migration_sensor": sensor_entry.unit_of_measurement, + "migration_stats": sensor_stat.get(self.selected_sensor), + "migration_count": self.count, + }, + ) + + # retain windy data + user_input.update(self.windy_data) + + # retain user_data + user_input.update(self.user_data) + + # retain senors + user_input.update(self.sensors) + + return self.async_create_entry(title=DOMAIN, data=user_input) + + async def async_step_migration_complete(self, user_input=None): + """Migration complete.""" + + errors = {} + + await self._get_entry_data() + sensor_entry = er.async_get(self.hass).async_get(self.selected_sensor) + sensor_stat = await self.load_sensors_to_migrate() + + if user_input is None: + return self.async_show_form( + step_id="migration_complete", + data_schema=vol.Schema({}), errors=errors, description_placeholders={ - "migration_status": user_input.get(SENSOR_TO_MIGRATE), - "migration_count": count, + "migration_sensor": sensor_entry.unit_of_measurement, + "migration_stats": sensor_stat.get(self.selected_sensor), + "migration_count": self.count, }, ) From a07af5a4fd27ea8f0743fc4cb7c2f577331f64be Mon Sep 17 00:00:00 2001 From: Ferron Nijland Date: Wed, 9 Jul 2025 16:26:18 +0200 Subject: [PATCH 18/30] Add outside battery sensor and related translations --- custom_components/sws12500/const.py | 32 +++++++++++++++++++ custom_components/sws12500/sensors_wslink.py | 11 ++++++- custom_components/sws12500/strings.json | 1 + .../sws12500/translations/cs.json | 1 + .../sws12500/translations/en.json | 2 ++ custom_components/sws12500/utils.py | 13 ++++++++ 6 files changed, 59 insertions(+), 1 deletion(-) diff --git a/custom_components/sws12500/const.py b/custom_components/sws12500/const.py index 01a92d0..2ebc981 100644 --- a/custom_components/sws12500/const.py +++ b/custom_components/sws12500/const.py @@ -63,6 +63,7 @@ OUTSIDE_TEMP: Final = "outside_temp" DEW_POINT: Final = "dew_point" OUTSIDE_HUMIDITY: Final = "outside_humidity" OUTSIDE_CONNECTION: Final = "outside_connection" +OUTSIDE_BATTERY: Final = "outside_battery" WIND_SPEED: Final = "wind_speed" WIND_GUST: Final = "wind_gust" WIND_DIR: Final = "wind_dir" @@ -179,6 +180,26 @@ class UnitOfDir(StrEnum): N = "n" +class UnitOfDir(StrEnum): + """Wind direrction azimut.""" + + NNE = "nne" + NE = "ne" + ENE = "ene" + E = "e" + ESE = "ese" + SE = "se" + SSE = "sse" + S = "s" + SSW = "ssw" + SW = "sw" + WSW = "wsw" + W = "w" + WNW = "wnw" + NW = "nw" + NNW = "nnw" + N = "n" + AZIMUT: list[UnitOfDir] = [ UnitOfDir.NNE, UnitOfDir.NE, @@ -197,3 +218,14 @@ AZIMUT: list[UnitOfDir] = [ UnitOfDir.NNW, UnitOfDir.N, ] + +class UnitOfBat(StrEnum): + """Battery level unit of measure.""" + + LOW = "low" + NORMAL = "normal" + +BATLEVEL: list[UnitOfBat] = [ + UnitOfBat.LOW, + UnitOfBat.NORMAL, +] diff --git a/custom_components/sws12500/sensors_wslink.py b/custom_components/sws12500/sensors_wslink.py index 5d512e2..9f5e673 100644 --- a/custom_components/sws12500/sensors_wslink.py +++ b/custom_components/sws12500/sensors_wslink.py @@ -29,6 +29,7 @@ from .const import ( HEAT_INDEX, INDOOR_HUMIDITY, INDOOR_TEMP, + OUTSIDE_BATTERY, OUTSIDE_HUMIDITY, OUTSIDE_TEMP, RAIN, @@ -45,7 +46,7 @@ from .const import ( WEEKLY_RAIN, ) from .sensors_common import WeatherSensorEntityDescription -from .utils import wind_dir_to_text +from .utils import battery_level_to_text, wind_dir_to_text SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = ( WeatherSensorEntityDescription( @@ -303,4 +304,12 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = ( translation_key=CHILL_INDEX, value_fn=lambda data: cast("int", data), ), + WeatherSensorEntityDescription( + key=OUTSIDE_BATTERY, + name="Outside Battery", + icon="mdi:battery", + device_class=SensorDeviceClass.ENUM, + value_fn=lambda data: cast("str", battery_level_to_text(data)), + translation_key=OUTSIDE_BATTERY, + ), ) diff --git a/custom_components/sws12500/strings.json b/custom_components/sws12500/strings.json index fda0f39..7260ffa 100644 --- a/custom_components/sws12500/strings.json +++ b/custom_components/sws12500/strings.json @@ -135,6 +135,7 @@ } } }, + "outside_battery": { "name": "Outside battery level" }, "notify": { "added": { "title": "New sensors for SWS 12500 found.", diff --git a/custom_components/sws12500/translations/cs.json b/custom_components/sws12500/translations/cs.json index e024ec3..e6009ef 100644 --- a/custom_components/sws12500/translations/cs.json +++ b/custom_components/sws12500/translations/cs.json @@ -139,6 +139,7 @@ } } }, + "outside_battery": { "name": "Vnější úroveň nabití baterie" }, "notify": { "added": { "title": "Nalezeny nové senzory pro SWS 12500.", diff --git a/custom_components/sws12500/translations/en.json b/custom_components/sws12500/translations/en.json index e0ce1a6..1260904 100644 --- a/custom_components/sws12500/translations/en.json +++ b/custom_components/sws12500/translations/en.json @@ -136,6 +136,8 @@ "nw": "NW", "nnw": "NNW" } + }, + "outside_battery": { "name": "Outside battery level" } } } }, diff --git a/custom_components/sws12500/utils.py b/custom_components/sws12500/utils.py index 30a667b..b982910 100644 --- a/custom_components/sws12500/utils.py +++ b/custom_components/sws12500/utils.py @@ -20,6 +20,7 @@ from homeassistant.helpers.translation import async_get_translations from .const import ( AZIMUT, + BATLEVEL, DATABASE_PATH, DEV_DBG, OUTSIDE_HUMIDITY, @@ -180,7 +181,19 @@ def wind_dir_to_text(deg: float) -> UnitOfDir | None: return None +def battery_level_to_text(battery: int) -> str: + """Return battery level in text representation. + Returns UnitOfBat or None + """ + if battery is None: + return "unknown" + + if battery is 0: + return BATLEVEL[battery] + elif battery is 1: + return BATLEVEL[battery] + def fahrenheit_to_celsius(fahrenheit: float) -> float: """Convert Fahrenheit to Celsius.""" return (fahrenheit - 32) * 5.0 / 9.0 From b6080fe9fd289ea4dee5854795a537b54f845170 Mon Sep 17 00:00:00 2001 From: FerronN Date: Fri, 11 Jul 2025 10:15:44 +0200 Subject: [PATCH 19/30] Fix structure en.json --- custom_components/sws12500/translations/en.json | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/sws12500/translations/en.json b/custom_components/sws12500/translations/en.json index 1260904..f89b307 100644 --- a/custom_components/sws12500/translations/en.json +++ b/custom_components/sws12500/translations/en.json @@ -138,7 +138,6 @@ } }, "outside_battery": { "name": "Outside battery level" } - } } }, "notify": { From af286648e91929b2100975aff0aa7e74fe660bc2 Mon Sep 17 00:00:00 2001 From: FerronN Date: Fri, 11 Jul 2025 10:16:59 +0200 Subject: [PATCH 20/30] fix data parsing in sensors_wslink.py --- custom_components/sws12500/sensors_wslink.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sws12500/sensors_wslink.py b/custom_components/sws12500/sensors_wslink.py index 9f5e673..ac2d83d 100644 --- a/custom_components/sws12500/sensors_wslink.py +++ b/custom_components/sws12500/sensors_wslink.py @@ -309,7 +309,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = ( name="Outside Battery", icon="mdi:battery", device_class=SensorDeviceClass.ENUM, - value_fn=lambda data: cast("str", battery_level_to_text(data)), + value_fn=lambda data: battery_level_to_text(int(data)) if data is not None and str(data).isdigit() else "unknown", translation_key=OUTSIDE_BATTERY, ), ) From a68a4c929ac4a77745278a96f8ad5303ca93cf5c Mon Sep 17 00:00:00 2001 From: schizza Date: Sat, 16 Aug 2025 17:29:22 +0200 Subject: [PATCH 21/30] Update const.py --- custom_components/sws12500/const.py | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/custom_components/sws12500/const.py b/custom_components/sws12500/const.py index 2ebc981..42a5af8 100644 --- a/custom_components/sws12500/const.py +++ b/custom_components/sws12500/const.py @@ -159,27 +159,6 @@ DISABLED_BY_DEFAULT: Final = [ ] -class UnitOfDir(StrEnum): - """Wind direrction azimut.""" - - NNE = "nne" - NE = "ne" - ENE = "ene" - E = "e" - ESE = "ese" - SE = "se" - SSE = "sse" - S = "s" - SSW = "ssw" - SW = "sw" - WSW = "wsw" - W = "w" - WNW = "wnw" - NW = "nw" - NNW = "nnw" - N = "n" - - class UnitOfDir(StrEnum): """Wind direrction azimut.""" @@ -224,8 +203,10 @@ class UnitOfBat(StrEnum): LOW = "low" NORMAL = "normal" + UNKNOWN = "unknown" -BATLEVEL: list[UnitOfBat] = [ +BATTERY_LEVEL: list[UnitOfBat] = [ UnitOfBat.LOW, UnitOfBat.NORMAL, + UnitOfBat.UNKNOWN, ] From 3dbf8b8a7afd865660abe49aee5f26355cd90368 Mon Sep 17 00:00:00 2001 From: schizza Date: Sun, 17 Aug 2025 18:33:42 +0200 Subject: [PATCH 22/30] Improves battery level representation Refactors battery level representation by using enum instead of string. Improves battery level display by adding an icon representation. Changes const BATLEVEL to BATTERY_LEVEL. --- custom_components/sws12500/utils.py | 45 ++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/custom_components/sws12500/utils.py b/custom_components/sws12500/utils.py index b982910..c78f63f 100644 --- a/custom_components/sws12500/utils.py +++ b/custom_components/sws12500/utils.py @@ -20,7 +20,7 @@ from homeassistant.helpers.translation import async_get_translations from .const import ( AZIMUT, - BATLEVEL, + BATTERY_LEVEL, DATABASE_PATH, DEV_DBG, OUTSIDE_HUMIDITY, @@ -30,6 +30,7 @@ from .const import ( SENSORS_TO_LOAD, WIND_SPEED, UnitOfDir, + UnitOfBat, ) _LOGGER = logging.getLogger(__name__) @@ -181,19 +182,33 @@ def wind_dir_to_text(deg: float) -> UnitOfDir | None: return None -def battery_level_to_text(battery: int) -> str: + +def battery_level_to_text(battery: int) -> UnitOfBat: """Return battery level in text representation. - Returns UnitOfBat or None + Returns UnitOfBat """ - if battery is None: - return "unknown" - if battery is 0: - return BATLEVEL[battery] - elif battery is 1: - return BATLEVEL[battery] - + return { + 0: UnitOfBat.LOW, + 1: UnitOfBat.NORMAL, + }.get(battery, UnitOfBat.UNKNOWN) + + +def battery_level_to_icon(battery: UnitOfBat) -> str: + """Return battery level in icon representation. + + Returns str + """ + + icons = { + UnitOfBat.LOW: "mdi:battery-alert", + UnitOfBat.NORMAL: "mdi:battery", + } + + return icons.get(battery, "mdi:battery-unknown") + + def fahrenheit_to_celsius(fahrenheit: float) -> float: """Convert Fahrenheit to Celsius.""" return (fahrenheit - 32) * 5.0 / 9.0 @@ -280,10 +295,12 @@ def long_term_units_in_statistics_meta(): db = conn.cursor() try: - db.execute(""" + db.execute( + """ SELECT statistic_id, unit_of_measurement from statistics_meta WHERE statistic_id LIKE 'sensor.weather_station_sws%' - """) + """ + ) rows = db.fetchall() sensor_units = { statistic_id: f"{statistic_id} ({unit})" for statistic_id, unit in rows @@ -299,8 +316,8 @@ def long_term_units_in_statistics_meta(): async def migrate_data(hass: HomeAssistant, sensor_id: str | None = None) -> bool: """Migrate data from mm/d to mm.""" - - _LOGGER.debug("Sensor %s is required for data migration", sensor_id) + + _LOGGER.debug("Sensor %s is required for data migration", sensor_id) updated_rows = 0 if not Path(DATABASE_PATH).exists(): From af19358ac76a1c8d2b784f52a7cb97d3f9bef22f Mon Sep 17 00:00:00 2001 From: schizza Date: Mon, 18 Aug 2025 10:55:37 +0200 Subject: [PATCH 23/30] Adds battery state translations Adds translations for the battery state of the outside sensor to both the English and Czech language files. This change provides more descriptive and user-friendly information about the battery status. --- custom_components/sws12500/strings.json | 9 ++++++++- custom_components/sws12500/translations/cs.json | 9 ++++++++- custom_components/sws12500/translations/en.json | 9 ++++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/custom_components/sws12500/strings.json b/custom_components/sws12500/strings.json index 7260ffa..3472af0 100644 --- a/custom_components/sws12500/strings.json +++ b/custom_components/sws12500/strings.json @@ -131,11 +131,18 @@ "wnw": "WNW", "nw": "NW", "nnw": "NNW" + }, + "outside_battery": { + "name": "Outside battery level", + "state": { + "normal": "OK", + "low": "Low", + "unknown": "Unknown / drained out" + } } } } }, - "outside_battery": { "name": "Outside battery level" }, "notify": { "added": { "title": "New sensors for SWS 12500 found.", diff --git a/custom_components/sws12500/translations/cs.json b/custom_components/sws12500/translations/cs.json index e6009ef..8da9943 100644 --- a/custom_components/sws12500/translations/cs.json +++ b/custom_components/sws12500/translations/cs.json @@ -136,10 +136,17 @@ "nw": "SZ", "nnw": "SSZ" } + }, + "outside_battery": { + "name": "Stav nabití venkovní baterie", + "state": { + "low": "Nízká", + "normal": "Normální", + "unknown": "Neznámá / zcela vybitá" + } } } }, - "outside_battery": { "name": "Vnější úroveň nabití baterie" }, "notify": { "added": { "title": "Nalezeny nové senzory pro SWS 12500.", diff --git a/custom_components/sws12500/translations/en.json b/custom_components/sws12500/translations/en.json index f89b307..b0f3567 100644 --- a/custom_components/sws12500/translations/en.json +++ b/custom_components/sws12500/translations/en.json @@ -137,7 +137,14 @@ "nnw": "NNW" } }, - "outside_battery": { "name": "Outside battery level" } + "outside_battery": { + "name": "Outside battery level", + "state": { + "normal": "OK", + "low": "Low", + "unknown": "Unknown / drained out" + } + } } }, "notify": { From 0d0922a4940b9200d41d9867bea184513527c132 Mon Sep 17 00:00:00 2001 From: schizza Date: Mon, 18 Aug 2025 12:50:31 +0200 Subject: [PATCH 24/30] Improves battery sensor display Updates the outside battery sensor to display an icon reflecting the battery level, enhancing the user experience by providing a visual indication of the battery status. --- custom_components/sws12500/sensors_wslink.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/custom_components/sws12500/sensors_wslink.py b/custom_components/sws12500/sensors_wslink.py index ac2d83d..b360ff5 100644 --- a/custom_components/sws12500/sensors_wslink.py +++ b/custom_components/sws12500/sensors_wslink.py @@ -46,7 +46,7 @@ from .const import ( WEEKLY_RAIN, ) from .sensors_common import WeatherSensorEntityDescription -from .utils import battery_level_to_text, wind_dir_to_text +from .utils import battery_level_to_icon, battery_level_to_text, wind_dir_to_text SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = ( WeatherSensorEntityDescription( @@ -306,10 +306,9 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = ( ), WeatherSensorEntityDescription( key=OUTSIDE_BATTERY, - name="Outside Battery", - icon="mdi:battery", - device_class=SensorDeviceClass.ENUM, - value_fn=lambda data: battery_level_to_text(int(data)) if data is not None and str(data).isdigit() else "unknown", translation_key=OUTSIDE_BATTERY, + icon=lambda data: battery_level_to_icon(battery_level_to_text(int(data))), + device_class=SensorDeviceClass.ENUM, + value_fn=lambda data: battery_level_to_text(int(data)), ), ) From de013891c0440d2742716ddc8ae131c796a69ec6 Mon Sep 17 00:00:00 2001 From: schizza Date: Mon, 18 Aug 2025 12:53:40 +0200 Subject: [PATCH 25/30] Adds missing constant and improves readability Adds OUTSIDE_BATTERY to the DISABLED_BY_DEFAULT list. Improves readability by formatting long strings with parenthesis. --- custom_components/sws12500/const.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/custom_components/sws12500/const.py b/custom_components/sws12500/const.py index 42a5af8..f0faff9 100644 --- a/custom_components/sws12500/const.py +++ b/custom_components/sws12500/const.py @@ -23,8 +23,12 @@ WSLINK: Final = "wslink" WINDY_API_KEY = "WINDY_API_KEY" WINDY_ENABLED: Final = "windy_enabled_checkbox" WINDY_LOGGER_ENABLED: Final = "windy_logger_checkbox" -WINDY_NOT_INSERTED: Final = "Data was succefuly sent to Windy, but not inserted by Windy API. Does anyone else sent data to Windy?" -WINDY_INVALID_KEY: Final = "Windy API KEY is invalid. Send data to Windy is now disabled. Check your API KEY and try again." +WINDY_NOT_INSERTED: Final = ( + "Data was succefuly sent to Windy, but not inserted by Windy API. Does anyone else sent data to Windy?" +) +WINDY_INVALID_KEY: Final = ( + "Windy API KEY is invalid. Send data to Windy is now disabled. Check your API KEY and try again." +) WINDY_SUCCESS: Final = ( "Windy successfully sent data and data was successfully inserted by Windy API" ) @@ -156,6 +160,7 @@ DISABLED_BY_DEFAULT: Final = [ CH3_HUMIDITY, CH4_TEMP, CH4_HUMIDITY, + OUTSIDE_BATTERY, ] @@ -179,6 +184,7 @@ class UnitOfDir(StrEnum): NNW = "nnw" N = "n" + AZIMUT: list[UnitOfDir] = [ UnitOfDir.NNE, UnitOfDir.NE, @@ -198,6 +204,7 @@ AZIMUT: list[UnitOfDir] = [ UnitOfDir.N, ] + class UnitOfBat(StrEnum): """Battery level unit of measure.""" @@ -205,6 +212,7 @@ class UnitOfBat(StrEnum): NORMAL = "normal" UNKNOWN = "unknown" + BATTERY_LEVEL: list[UnitOfBat] = [ UnitOfBat.LOW, UnitOfBat.NORMAL, From 07ca4a683382ec7f3b4cf89e0ed4e7a35eb46717 Mon Sep 17 00:00:00 2001 From: schizza Date: Mon, 18 Aug 2025 13:29:00 +0200 Subject: [PATCH 26/30] Reorders constants for better readability Reorders the import of constants to improve readability and maintain consistency within the module. Final touches. --- custom_components/sws12500/sensors_wslink.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/sws12500/sensors_wslink.py b/custom_components/sws12500/sensors_wslink.py index b360ff5..0ef59ac 100644 --- a/custom_components/sws12500/sensors_wslink.py +++ b/custom_components/sws12500/sensors_wslink.py @@ -27,23 +27,23 @@ from .const import ( DAILY_RAIN, DEW_POINT, HEAT_INDEX, + HOURLY_RAIN, INDOOR_HUMIDITY, INDOOR_TEMP, + MONTHLY_RAIN, OUTSIDE_BATTERY, OUTSIDE_HUMIDITY, OUTSIDE_TEMP, RAIN, SOLAR_RADIATION, UV, + WEEKLY_RAIN, WIND_AZIMUT, WIND_DIR, WIND_GUST, WIND_SPEED, - UnitOfDir, - MONTHLY_RAIN, YEARLY_RAIN, - HOURLY_RAIN, - WEEKLY_RAIN, + UnitOfDir, ) from .sensors_common import WeatherSensorEntityDescription from .utils import battery_level_to_icon, battery_level_to_text, wind_dir_to_text From b858f648b9a382a8a42895f28384b49972949d61 Mon Sep 17 00:00:00 2001 From: schizza Date: Thu, 21 Aug 2025 16:30:10 +0200 Subject: [PATCH 27/30] config_flow migrated to stable version. Config flow was migrated to stable version. Removes the unit migration flow, which is intended to introduce later. --- custom_components/sws12500/config_flow.py | 192 +--------------------- 1 file changed, 2 insertions(+), 190 deletions(-) diff --git a/custom_components/sws12500/config_flow.py b/custom_components/sws12500/config_flow.py index 9e6b2b2..3b693da 100644 --- a/custom_components/sws12500/config_flow.py +++ b/custom_components/sws12500/config_flow.py @@ -1,15 +1,13 @@ """Config flow for Sencor SWS 12500 Weather Station integration.""" -import logging from typing import Any import voluptuous as vol from homeassistant.config_entries import ConfigFlow, OptionsFlow -from homeassistant.const import UnitOfPrecipitationDepth, UnitOfVolumetricFlux from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.entity_registry as er +from .utils import long_term_units_in_statistics_meta, migrate_data from .const import ( API_ID, @@ -17,18 +15,13 @@ from .const import ( DEV_DBG, DOMAIN, INVALID_CREDENTIALS, - MIG_FROM, - MIG_TO, - SENSOR_TO_MIGRATE, SENSORS_TO_LOAD, + SENSOR_TO_MIGRATE, WINDY_API_KEY, WINDY_ENABLED, WINDY_LOGGER_ENABLED, WSLINK, ) -from .utils import long_term_units_in_statistics_meta, migrate_data - -_LOGGER = logging.getLogger(__name__) class CannotConnect(HomeAssistantError): @@ -52,13 +45,6 @@ class ConfigOptionsFlowHandler(OptionsFlow): self.user_data_schema = {} self.sensors: dict[str, Any] = {} self.migrate_schema = {} - self.migrate_sensor_select = {} - self.migrate_unit_selection = {} - self.count = 0 - self.selected_sensor = "" - - self.unit_values = [unit.value for unit in UnitOfVolumetricFlux] - self.unit_values.extend([unit.value for unit in UnitOfPrecipitationDepth]) @property def config_entry(self): @@ -107,35 +93,6 @@ class ConfigOptionsFlowHandler(OptionsFlow): ): bool or False, } - self.migrate_sensor_select = { - vol.Required(SENSOR_TO_MIGRATE): vol.In( - await self.load_sensors_to_migrate() or {} - ), - } - - self.migrate_unit_selection = { - vol.Required(MIG_FROM): vol.In(self.unit_values), - vol.Required(MIG_TO): vol.In(self.unit_values), - vol.Optional("trigger_action", default=False): bool, - } - # "mm/d", "mm/h", "mm", "in/d", "in/h", "in" - - async def load_sensors_to_migrate(self) -> dict[str, Any]: - """Load sensors to migrate.""" - - sensor_statistics = await long_term_units_in_statistics_meta(self.hass) - - entity_registry = er.async_get(self.hass) - sensors = entity_registry.entities.get_entries_for_config_entry_id( - self.config_entry.entry_id - ) - - return { - sensor.entity_id: f"{sensor.name or sensor.original_name} (current settings: {sensor.unit_of_measurement}, longterm stats unit: {sensor_statistics.get(sensor.entity_id)})" - for sensor in sensors - if sensor.unique_id in {"rain", "daily_rain"} - } - async def async_step_init(self, user_input=None): """Manage the options - show menu first.""" return self.async_show_menu( @@ -208,151 +165,6 @@ class ConfigOptionsFlowHandler(OptionsFlow): return self.async_create_entry(title=DOMAIN, data=user_input) - async def async_step_migration(self, user_input=None): - """Migrate sensors.""" - - errors = {} - - data_schema = vol.Schema(self.migrate_sensor_select) - data_schema.schema.update() - - await self._get_entry_data() - - if user_input is None: - return self.async_show_form( - step_id="migration", - data_schema=vol.Schema(self.migrate_sensor_select), - errors=errors, - description_placeholders={ - "migration_status": "-", - "migration_count": "-", - }, - ) - - self.selected_sensor = user_input.get(SENSOR_TO_MIGRATE) - - return await self.async_step_migration_units() - - async def async_step_migration_units(self, user_input=None): - """Migrate unit step.""" - - registry = er.async_get(self.hass) - sensor_entry = registry.async_get(self.selected_sensor) - sensor_stats = await long_term_units_in_statistics_meta(self.hass) - - default_unit = sensor_entry.unit_of_measurement if sensor_entry else None - - if default_unit not in self.unit_values: - default_unit = self.unit_values[0] - - data_schema = vol.Schema({ - vol.Required(MIG_FROM, default=default_unit): vol.In(self.unit_values), - vol.Required(MIG_TO): vol.In(self.unit_values), - vol.Optional("trigger_action", default=False): bool, - }) - - if user_input is None: - return self.async_show_form( - step_id="migration_units", - data_schema=data_schema, - errors={}, - description_placeholders={ - "migration_sensor": sensor_entry.original_name, - "migration_stats": sensor_stats.get(self.selected_sensor), - }, - ) - - if user_input.get("trigger_action"): - self.count = await migrate_data( - self.hass, - self.selected_sensor, - user_input.get(MIG_FROM), - user_input.get(MIG_TO), - ) - - registry.async_update_entity(self.selected_sensor, - unit_of_measurement=user_input.get(MIG_TO), - ) - - state = self.hass.states.get(self.selected_sensor) - if state: - _LOGGER.info("State attributes before update: %s", state.attributes) - attributes = dict(state.attributes) - attributes["unit_of_measurement"] = user_input.get(MIG_TO) - self.hass.states.async_set(self.selected_sensor, state.state, attributes) - _LOGGER.info("State attributes after update: %s", attributes) - - options = {**self.config_entry.options, "reload_sensor": self.selected_sensor} - self.hass.config_entries.async_update_entry(self.config_entry, options=options) - - await self.hass.config_entries.async_reload(self.config_entry.entry_id) - - await self.hass.async_block_till_done() - - _LOGGER.info("Migration complete for sensor %s: %s row updated, new measurement unit: %s, ", - self.selected_sensor, - self.count, - user_input.get(MIG_TO), - ) - - await self._get_entry_data() - sensor_entry = er.async_get(self.hass).async_get(self.selected_sensor) - sensor_stat = await self.load_sensors_to_migrate() - - return self.async_show_form( - step_id="migration_complete", - data_schema=vol.Schema({}), - errors={}, - description_placeholders={ - "migration_sensor": sensor_entry.unit_of_measurement, - "migration_stats": sensor_stat.get(self.selected_sensor), - "migration_count": self.count, - }, - ) - - # retain windy data - user_input.update(self.windy_data) - - # retain user_data - user_input.update(self.user_data) - - # retain senors - user_input.update(self.sensors) - - return self.async_create_entry(title=DOMAIN, data=user_input) - - async def async_step_migration_complete(self, user_input=None): - """Migration complete.""" - - errors = {} - - await self._get_entry_data() - sensor_entry = er.async_get(self.hass).async_get(self.selected_sensor) - sensor_stat = await self.load_sensors_to_migrate() - - if user_input is None: - return self.async_show_form( - step_id="migration_complete", - data_schema=vol.Schema({}), - errors=errors, - description_placeholders={ - "migration_sensor": sensor_entry.unit_of_measurement, - "migration_stats": sensor_stat.get(self.selected_sensor), - "migration_count": self.count, - }, - ) - - # retain windy data - user_input.update(self.windy_data) - - # retain user_data - user_input.update(self.user_data) - - # retain senors - user_input.update(self.sensors) - - return self.async_create_entry(title=DOMAIN, data=user_input) - class ConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Sencor SWS 12500 Weather Station.""" From 720c2148e697afc1f79a64170b0549fa8136be93 Mon Sep 17 00:00:00 2001 From: schizza Date: Thu, 21 Aug 2025 16:45:29 +0200 Subject: [PATCH 28/30] Removes unused import from config_flow Removes the unused import of `utils` to improve code cleanliness and avoid potential namespace conflicts. Removed 'migration' from menu as it is intended to use in later version. --- custom_components/sws12500/config_flow.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/sws12500/config_flow.py b/custom_components/sws12500/config_flow.py index 3b693da..342589c 100644 --- a/custom_components/sws12500/config_flow.py +++ b/custom_components/sws12500/config_flow.py @@ -7,7 +7,6 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow, OptionsFlow from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from .utils import long_term_units_in_statistics_meta, migrate_data from .const import ( API_ID, From 64dd47a3e9cbda2907e4845c72710b640419e0c7 Mon Sep 17 00:00:00 2001 From: schizza Date: Thu, 21 Aug 2025 16:46:13 +0200 Subject: [PATCH 29/30] Option flow configuration Removes the "migration" step from the option flow menu. This step will be used in next release. --- custom_components/sws12500/config_flow.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/custom_components/sws12500/config_flow.py b/custom_components/sws12500/config_flow.py index 342589c..0717ccf 100644 --- a/custom_components/sws12500/config_flow.py +++ b/custom_components/sws12500/config_flow.py @@ -15,7 +15,6 @@ from .const import ( DOMAIN, INVALID_CREDENTIALS, SENSORS_TO_LOAD, - SENSOR_TO_MIGRATE, WINDY_API_KEY, WINDY_ENABLED, WINDY_LOGGER_ENABLED, @@ -67,9 +66,11 @@ class ConfigOptionsFlowHandler(OptionsFlow): } self.sensors: dict[str, Any] = { - SENSORS_TO_LOAD: self.config_entry.options.get(SENSORS_TO_LOAD) - if isinstance(self.config_entry.options.get(SENSORS_TO_LOAD), list) - else [] + SENSORS_TO_LOAD: ( + self.config_entry.options.get(SENSORS_TO_LOAD) + if isinstance(self.config_entry.options.get(SENSORS_TO_LOAD), list) + else [] + ) } self.windy_data: dict[str, Any] = { @@ -89,14 +90,13 @@ class ConfigOptionsFlowHandler(OptionsFlow): vol.Optional( WINDY_LOGGER_ENABLED, default=self.windy_data[WINDY_LOGGER_ENABLED], - ): bool or False, + ): bool + or False, } async def async_step_init(self, user_input=None): """Manage the options - show menu first.""" - return self.async_show_menu( - step_id="init", menu_options=["basic", "windy", "migration"] - ) + return self.async_show_menu(step_id="init", menu_options=["basic", "windy"]) async def async_step_basic(self, user_input=None): """Manage basic options - credentials.""" From 99fd6d266c0634a3f20657452e996cc521a65dc5 Mon Sep 17 00:00:00 2001 From: schizza Date: Fri, 22 Aug 2025 18:06:35 +0200 Subject: [PATCH 30/30] Adds outside battery sensor Adds an outside battery sensor to the integration, providing information about the battery level of the outdoor sensor. This includes: - Mapping the `t1bat` WSLink item to the `OUTSIDE_BATTERY` sensor. - Implementing logic to convert the battery level to a human-readable text representation and a corresponding icon. - Updates precipitation to intensity and fixes data type of battery level --- custom_components/sws12500/const.py | 1 + custom_components/sws12500/sensor.py | 19 +++++++++++++++++-- custom_components/sws12500/sensors_wslink.py | 6 +++--- custom_components/sws12500/utils.py | 4 ++-- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/custom_components/sws12500/const.py b/custom_components/sws12500/const.py index f0faff9..afe1979 100644 --- a/custom_components/sws12500/const.py +++ b/custom_components/sws12500/const.py @@ -144,6 +144,7 @@ REMAP_WSLINK_ITEMS: dict = { "t1rainyr": YEARLY_RAIN, "t234c2tem": CH3_TEMP, "t234c2hum": CH3_HUMIDITY, + "t1bat": OUTSIDE_BATTERY, } # TODO: Add more sensors diff --git a/custom_components/sws12500/sensor.py b/custom_components/sws12500/sensor.py index a0275a2..0be5eff 100644 --- a/custom_components/sws12500/sensor.py +++ b/custom_components/sws12500/sensor.py @@ -15,6 +15,7 @@ from .const import ( CHILL_INDEX, DOMAIN, HEAT_INDEX, + OUTSIDE_BATTERY, OUTSIDE_HUMIDITY, OUTSIDE_TEMP, SENSORS_TO_LOAD, @@ -26,7 +27,7 @@ from .const import ( from .sensors_common import WeatherSensorEntityDescription from .sensors_weather import SENSOR_TYPES_WEATHER_API from .sensors_wslink import SENSOR_TYPES_WSLINK -from .utils import chill_index, heat_index +from .utils import chill_index, heat_index, battery_level_to_icon, battery_level_to_text _LOGGER = logging.getLogger(__name__) @@ -130,13 +131,27 @@ class WeatherSensor( ): return self.entity_description.value_fn(chill_index(self.coordinator.data)) - return None if self._data == "" else self.entity_description.value_fn(self._data) + return ( + None if self._data == "" else self.entity_description.value_fn(self._data) + ) @property def suggested_entity_id(self) -> str: """Return name.""" return generate_entity_id("sensor.{}", self.entity_description.key) + @property + def icon(self) -> str | None: + """Return the dynamic icon for battery representation.""" + + if self.entity_description.key == OUTSIDE_BATTERY: + try: + return battery_level_to_icon(self.native_value) + except Exception: + return "mdi:battery-unknown" + + return self.entity_description.icon + @property def device_info(self) -> DeviceInfo: """Device info.""" diff --git a/custom_components/sws12500/sensors_wslink.py b/custom_components/sws12500/sensors_wslink.py index 0ef59ac..f0d3c7a 100644 --- a/custom_components/sws12500/sensors_wslink.py +++ b/custom_components/sws12500/sensors_wslink.py @@ -144,7 +144,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = ( WeatherSensorEntityDescription( key=RAIN, native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, - device_class=SensorDeviceClass.PRECIPITATION, + device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, state_class=SensorStateClass.TOTAL, suggested_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, suggested_display_precision=2, @@ -307,8 +307,8 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = ( WeatherSensorEntityDescription( key=OUTSIDE_BATTERY, translation_key=OUTSIDE_BATTERY, - icon=lambda data: battery_level_to_icon(battery_level_to_text(int(data))), + icon="mdi:battery-unknown", device_class=SensorDeviceClass.ENUM, - value_fn=lambda data: battery_level_to_text(int(data)), + value_fn=lambda data: battery_level_to_text(data), ), ) diff --git a/custom_components/sws12500/utils.py b/custom_components/sws12500/utils.py index c78f63f..52fe68f 100644 --- a/custom_components/sws12500/utils.py +++ b/custom_components/sws12500/utils.py @@ -192,7 +192,7 @@ def battery_level_to_text(battery: int) -> UnitOfBat: return { 0: UnitOfBat.LOW, 1: UnitOfBat.NORMAL, - }.get(battery, UnitOfBat.UNKNOWN) + }.get(int(battery) if battery is not None else None, UnitOfBat.UNKNOWN) def battery_level_to_icon(battery: UnitOfBat) -> str: @@ -202,7 +202,7 @@ def battery_level_to_icon(battery: UnitOfBat) -> str: """ icons = { - UnitOfBat.LOW: "mdi:battery-alert", + UnitOfBat.LOW: "mdi:battery-low", UnitOfBat.NORMAL: "mdi:battery", }