diff --git a/custom_components/sws12500/config_flow.py b/custom_components/sws12500/config_flow.py index 9e6b2b2..0717ccf 100644 --- a/custom_components/sws12500/config_flow.py +++ b/custom_components/sws12500/config_flow.py @@ -1,15 +1,12 @@ """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 .const import ( API_ID, @@ -17,18 +14,12 @@ from .const import ( DEV_DBG, DOMAIN, INVALID_CREDENTIALS, - 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): @@ -52,13 +43,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): @@ -82,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] = { @@ -104,43 +90,13 @@ class ConfigOptionsFlowHandler(OptionsFlow): vol.Optional( WINDY_LOGGER_ENABLED, default=self.windy_data[WINDY_LOGGER_ENABLED], - ): 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"} + ): 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.""" @@ -208,151 +164,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.""" diff --git a/custom_components/sws12500/const.py b/custom_components/sws12500/const.py index 1819c7f..afe1979 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" ) @@ -63,6 +67,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 +142,9 @@ REMAP_WSLINK_ITEMS: dict = { "t1rainwy": WEEKLY_RAIN, "t1rainmth": MONTHLY_RAIN, "t1rainyr": YEARLY_RAIN, + "t234c2tem": CH3_TEMP, + "t234c2hum": CH3_HUMIDITY, + "t1bat": OUTSIDE_BATTERY, } # TODO: Add more sensors @@ -153,6 +161,7 @@ DISABLED_BY_DEFAULT: Final = [ CH3_HUMIDITY, CH4_TEMP, CH4_HUMIDITY, + OUTSIDE_BATTERY, ] @@ -195,3 +204,18 @@ AZIMUT: list[UnitOfDir] = [ UnitOfDir.NNW, UnitOfDir.N, ] + + +class UnitOfBat(StrEnum): + """Battery level unit of measure.""" + + LOW = "low" + NORMAL = "normal" + UNKNOWN = "unknown" + + +BATTERY_LEVEL: list[UnitOfBat] = [ + UnitOfBat.LOW, + UnitOfBat.NORMAL, + UnitOfBat.UNKNOWN, +] diff --git a/custom_components/sws12500/manifest.json b/custom_components/sws12500/manifest.json index 56461c7..7173243 100644 --- a/custom_components/sws12500/manifest.json +++ b/custom_components/sws12500/manifest.json @@ -10,6 +10,6 @@ "issue_tracker": "https://github.com/schizza/SWS-12500-custom-component/issues", "requirements": [], "ssdp": [], - "version": "1.6.2", + "version": "1.6.6", "zeroconf": [] } 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 91e6c50..91a712a 100644 --- a/custom_components/sws12500/sensors_wslink.py +++ b/custom_components/sws12500/sensors_wslink.py @@ -27,25 +27,26 @@ 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 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( @@ -144,7 +145,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, @@ -244,25 +245,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, @@ -304,4 +305,11 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = ( translation_key=CHILL_INDEX, value_fn=lambda data: cast("int", data), ), + WeatherSensorEntityDescription( + key=OUTSIDE_BATTERY, + translation_key=OUTSIDE_BATTERY, + icon="mdi:battery-unknown", + device_class=SensorDeviceClass.ENUM, + value_fn=lambda data: battery_level_to_text(data), + ), ) diff --git a/custom_components/sws12500/strings.json b/custom_components/sws12500/strings.json index fda0f39..3472af0 100644 --- a/custom_components/sws12500/strings.json +++ b/custom_components/sws12500/strings.json @@ -131,6 +131,14 @@ "wnw": "WNW", "nw": "NW", "nnw": "NNW" + }, + "outside_battery": { + "name": "Outside battery level", + "state": { + "normal": "OK", + "low": "Low", + "unknown": "Unknown / drained out" + } } } } diff --git a/custom_components/sws12500/translations/cs.json b/custom_components/sws12500/translations/cs.json index e024ec3..8da9943 100644 --- a/custom_components/sws12500/translations/cs.json +++ b/custom_components/sws12500/translations/cs.json @@ -136,6 +136,14 @@ "nw": "SZ", "nnw": "SSZ" } + }, + "outside_battery": { + "name": "Stav nabití venkovní baterie", + "state": { + "low": "Nízká", + "normal": "Normální", + "unknown": "Neznámá / zcela vybitá" + } } } }, diff --git a/custom_components/sws12500/translations/en.json b/custom_components/sws12500/translations/en.json index e0ce1a6..b0f3567 100644 --- a/custom_components/sws12500/translations/en.json +++ b/custom_components/sws12500/translations/en.json @@ -136,6 +136,14 @@ "nw": "NW", "nnw": "NNW" } + }, + "outside_battery": { + "name": "Outside battery level", + "state": { + "normal": "OK", + "low": "Low", + "unknown": "Unknown / drained out" + } } } }, diff --git a/custom_components/sws12500/utils.py b/custom_components/sws12500/utils.py index 30a667b..52fe68f 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, + BATTERY_LEVEL, DATABASE_PATH, DEV_DBG, OUTSIDE_HUMIDITY, @@ -29,6 +30,7 @@ from .const import ( SENSORS_TO_LOAD, WIND_SPEED, UnitOfDir, + UnitOfBat, ) _LOGGER = logging.getLogger(__name__) @@ -181,6 +183,32 @@ def wind_dir_to_text(deg: float) -> UnitOfDir | None: return None +def battery_level_to_text(battery: int) -> UnitOfBat: + """Return battery level in text representation. + + Returns UnitOfBat + """ + + return { + 0: UnitOfBat.LOW, + 1: UnitOfBat.NORMAL, + }.get(int(battery) if battery is not None else None, UnitOfBat.UNKNOWN) + + +def battery_level_to_icon(battery: UnitOfBat) -> str: + """Return battery level in icon representation. + + Returns str + """ + + icons = { + UnitOfBat.LOW: "mdi:battery-low", + 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 @@ -267,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 @@ -286,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():