Compare commits

..

1 Commits

Author SHA1 Message Date
FerronN e832a82196
Merge e11e068c0f into 99d25bfd56 2025-08-18 11:29:12 +00:00
6 changed files with 224 additions and 52 deletions

View File

@ -1,12 +1,15 @@
"""Config flow for Sencor SWS 12500 Weather Station integration.""" """Config flow for Sencor SWS 12500 Weather Station integration."""
import logging
from typing import Any from typing import Any
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, OptionsFlow from homeassistant.config_entries import ConfigFlow, OptionsFlow
from homeassistant.const import UnitOfPrecipitationDepth, UnitOfVolumetricFlux
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.entity_registry as er
from .const import ( from .const import (
API_ID, API_ID,
@ -14,12 +17,18 @@ from .const import (
DEV_DBG, DEV_DBG,
DOMAIN, DOMAIN,
INVALID_CREDENTIALS, INVALID_CREDENTIALS,
MIG_FROM,
MIG_TO,
SENSOR_TO_MIGRATE,
SENSORS_TO_LOAD, SENSORS_TO_LOAD,
WINDY_API_KEY, WINDY_API_KEY,
WINDY_ENABLED, WINDY_ENABLED,
WINDY_LOGGER_ENABLED, WINDY_LOGGER_ENABLED,
WSLINK, WSLINK,
) )
from .utils import long_term_units_in_statistics_meta, migrate_data
_LOGGER = logging.getLogger(__name__)
class CannotConnect(HomeAssistantError): class CannotConnect(HomeAssistantError):
@ -43,6 +52,13 @@ class ConfigOptionsFlowHandler(OptionsFlow):
self.user_data_schema = {} self.user_data_schema = {}
self.sensors: dict[str, Any] = {} self.sensors: dict[str, Any] = {}
self.migrate_schema = {} 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 @property
def config_entry(self): def config_entry(self):
@ -66,11 +82,9 @@ class ConfigOptionsFlowHandler(OptionsFlow):
} }
self.sensors: dict[str, Any] = { self.sensors: dict[str, Any] = {
SENSORS_TO_LOAD: ( SENSORS_TO_LOAD: self.config_entry.options.get(SENSORS_TO_LOAD)
self.config_entry.options.get(SENSORS_TO_LOAD) if isinstance(self.config_entry.options.get(SENSORS_TO_LOAD), list)
if isinstance(self.config_entry.options.get(SENSORS_TO_LOAD), list) else []
else []
)
} }
self.windy_data: dict[str, Any] = { self.windy_data: dict[str, Any] = {
@ -90,13 +104,43 @@ class ConfigOptionsFlowHandler(OptionsFlow):
vol.Optional( vol.Optional(
WINDY_LOGGER_ENABLED, WINDY_LOGGER_ENABLED,
default=self.windy_data[WINDY_LOGGER_ENABLED], default=self.windy_data[WINDY_LOGGER_ENABLED],
): bool ): bool or False,
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): async def async_step_init(self, user_input=None):
"""Manage the options - show menu first.""" """Manage the options - show menu first."""
return self.async_show_menu(step_id="init", menu_options=["basic", "windy"]) return self.async_show_menu(
step_id="init", menu_options=["basic", "windy", "migration"]
)
async def async_step_basic(self, user_input=None): async def async_step_basic(self, user_input=None):
"""Manage basic options - credentials.""" """Manage basic options - credentials."""
@ -164,6 +208,151 @@ class ConfigOptionsFlowHandler(OptionsFlow):
return self.async_create_entry(title=DOMAIN, data=user_input) 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): class ConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Sencor SWS 12500 Weather Station.""" """Handle a config flow for Sencor SWS 12500 Weather Station."""

View File

@ -142,8 +142,6 @@ REMAP_WSLINK_ITEMS: dict = {
"t1rainwy": WEEKLY_RAIN, "t1rainwy": WEEKLY_RAIN,
"t1rainmth": MONTHLY_RAIN, "t1rainmth": MONTHLY_RAIN,
"t1rainyr": YEARLY_RAIN, "t1rainyr": YEARLY_RAIN,
"t234c2tem": CH3_TEMP,
"t234c2hum": CH3_HUMIDITY,
"t1bat": OUTSIDE_BATTERY, "t1bat": OUTSIDE_BATTERY,
} }

View File

@ -10,6 +10,6 @@
"issue_tracker": "https://github.com/schizza/SWS-12500-custom-component/issues", "issue_tracker": "https://github.com/schizza/SWS-12500-custom-component/issues",
"requirements": [], "requirements": [],
"ssdp": [], "ssdp": [],
"version": "1.6.5", "version": "1.6.2",
"zeroconf": [] "zeroconf": []
} }

View File

@ -15,7 +15,6 @@ from .const import (
CHILL_INDEX, CHILL_INDEX,
DOMAIN, DOMAIN,
HEAT_INDEX, HEAT_INDEX,
OUTSIDE_BATTERY,
OUTSIDE_HUMIDITY, OUTSIDE_HUMIDITY,
OUTSIDE_TEMP, OUTSIDE_TEMP,
SENSORS_TO_LOAD, SENSORS_TO_LOAD,
@ -27,7 +26,7 @@ from .const import (
from .sensors_common import WeatherSensorEntityDescription from .sensors_common import WeatherSensorEntityDescription
from .sensors_weather import SENSOR_TYPES_WEATHER_API from .sensors_weather import SENSOR_TYPES_WEATHER_API
from .sensors_wslink import SENSOR_TYPES_WSLINK from .sensors_wslink import SENSOR_TYPES_WSLINK
from .utils import chill_index, heat_index, battery_level_to_icon, battery_level_to_text from .utils import chill_index, heat_index
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -131,27 +130,13 @@ class WeatherSensor(
): ):
return self.entity_description.value_fn(chill_index(self.coordinator.data)) return self.entity_description.value_fn(chill_index(self.coordinator.data))
return ( return None if self._data == "" else self.entity_description.value_fn(self._data)
None if self._data == "" else self.entity_description.value_fn(self._data)
)
@property @property
def suggested_entity_id(self) -> str: def suggested_entity_id(self) -> str:
"""Return name.""" """Return name."""
return generate_entity_id("sensor.{}", self.entity_description.key) 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 @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Device info.""" """Device info."""

View File

@ -144,7 +144,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=RAIN, key=RAIN,
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
suggested_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, suggested_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
suggested_display_precision=2, suggested_display_precision=2,
@ -244,25 +244,25 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
translation_key=CH2_HUMIDITY, translation_key=CH2_HUMIDITY,
value_fn=lambda data: cast("int", data), value_fn=lambda data: cast("int", data),
), ),
WeatherSensorEntityDescription( # WeatherSensorEntityDescription(
key=CH3_TEMP, # key=CH3_TEMP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, # native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
state_class=SensorStateClass.MEASUREMENT, # state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE, # device_class=SensorDeviceClass.TEMPERATURE,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS, # suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
icon="mdi:weather-sunny", # icon="mdi:weather-sunny",
translation_key=CH3_TEMP, # translation_key=CH3_TEMP,
value_fn=lambda data: cast(float, data), # value_fn=lambda data: cast(float, data),
), # ),
WeatherSensorEntityDescription( # WeatherSensorEntityDescription(
key=CH3_HUMIDITY, # key=CH3_HUMIDITY,
native_unit_of_measurement=PERCENTAGE, # native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT, # state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.HUMIDITY, # device_class=SensorDeviceClass.HUMIDITY,
icon="mdi:weather-sunny", # icon="mdi:weather-sunny",
translation_key=CH3_HUMIDITY, # translation_key=CH3_HUMIDITY,
value_fn=lambda data: cast(int, data), # value_fn=lambda data: cast(int, data),
), # ),
# WeatherSensorEntityDescription( # WeatherSensorEntityDescription(
# key=CH4_TEMP, # key=CH4_TEMP,
# native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, # native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
@ -307,8 +307,8 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=OUTSIDE_BATTERY, key=OUTSIDE_BATTERY,
translation_key=OUTSIDE_BATTERY, translation_key=OUTSIDE_BATTERY,
icon="mdi:battery-unknown", icon=lambda data: battery_level_to_icon(battery_level_to_text(int(data))),
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
value_fn=lambda data: battery_level_to_text(data), value_fn=lambda data: battery_level_to_text(int(data)),
), ),
) )

View File

@ -192,7 +192,7 @@ def battery_level_to_text(battery: int) -> UnitOfBat:
return { return {
0: UnitOfBat.LOW, 0: UnitOfBat.LOW,
1: UnitOfBat.NORMAL, 1: UnitOfBat.NORMAL,
}.get(int(battery) if battery is not None else None, UnitOfBat.UNKNOWN) }.get(battery, UnitOfBat.UNKNOWN)
def battery_level_to_icon(battery: UnitOfBat) -> str: def battery_level_to_icon(battery: UnitOfBat) -> str:
@ -202,7 +202,7 @@ def battery_level_to_icon(battery: UnitOfBat) -> str:
""" """
icons = { icons = {
UnitOfBat.LOW: "mdi:battery-low", UnitOfBat.LOW: "mdi:battery-alert",
UnitOfBat.NORMAL: "mdi:battery", UnitOfBat.NORMAL: "mdi:battery",
} }