Compare commits

...

5 Commits
v1.6.3 ... main

Author SHA1 Message Date
Lukas Svoboda ee7be37ba6 Add shared hass.data keys/constants for cleaner integration state storage (local) 2026-01-20 20:01:29 +01:00
Lukas Svoboda 8034ada12f Add shared hass.data keys/constants for cleaner integration state storage 2026-01-20 19:59:29 +01:00
schizza 99d25bfd56 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.
2025-04-24 17:56:47 +02:00
Lukas Svoboda af87fd0719
Merge pull request #69 from schizza/wslink_name_fix1
WSLINK_name_fix
2025-04-13 17:54:53 +02:00
schizza 0ab5321170 Corrects translation keys for rain sensors
Updates the translation keys for hourly, weekly, monthly, and yearly rain sensors to ensure they accurately reflect the corresponding time periods.
2025-04-13 17:52:58 +02:00
3 changed files with 183 additions and 30 deletions

View File

@ -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,
},
)

View File

@ -170,7 +170,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
suggested_display_precision=2,
icon="mdi:weather-pouring",
translation_key=DAILY_RAIN,
translation_key=HOURLY_RAIN,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
@ -181,7 +181,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
suggested_display_precision=2,
icon="mdi:weather-pouring",
translation_key=DAILY_RAIN,
translation_key=WEEKLY_RAIN,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
@ -192,7 +192,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
suggested_display_precision=2,
icon="mdi:weather-pouring",
translation_key=DAILY_RAIN,
translation_key=MONTHLY_RAIN,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
@ -203,7 +203,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
suggested_display_precision=2,
icon="mdi:weather-pouring",
translation_key=DAILY_RAIN,
translation_key=YEARLY_RAIN,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(

View File

@ -0,0 +1,17 @@
"""Shared keys and helpers for storing integration runtime state in hass.data.
This integration uses `hass.data[DOMAIN][entry_id]` as a per-entry dictionary.
Keeping keys in one place prevents subtle bugs where different modules store
different value types under the same key.
"""
from __future__ import annotations
from typing import Final
# Per-entry dict keys stored under hass.data[DOMAIN][entry_id]
ENTRY_COORDINATOR: Final[str] = "coordinator"
ENTRY_ADD_ENTITIES: Final[str] = "async_add_entities"
ENTRY_DESCRIPTIONS: Final[str] = "sensor_descriptions"
ENTRY_LAST_OPTIONS: Final[str] = "last_options"