Merge branch 'main' into patch-1

pull/65/head
Lukas Svoboda 2025-08-28 21:57:38 +02:00 committed by GitHub
commit 7ff8bb7f92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 368 additions and 37 deletions

5
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,5 @@
# These are supported funding model platforms
github: schizza
ko_fi: schizza
buy_me_a_coffee: schizza

34
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,34 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
Provide `Developer log` if applicable
**Provide information about your station:**
- Weather station type:
- firmware version:
- [ ] Using PWS protocol
- [ ] Using WSLink API
- [ ] Using WSLink proxy Add-on
**Additional context**
Add any other context about the problem here.

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1 @@
blank_issues_enabled: false

View File

@ -0,0 +1,23 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Is your feature request a new addition**
Describe what you want to achieve. How new feature should work.
**Additional context**
Add any other context or screenshots about the feature request here.

34
.github/ISSUE_TEMPLATE/issue.md vendored Normal file
View File

@ -0,0 +1,34 @@
---
name: Issue
about: A minor issue that does not significantly affect functionality.
title: "[ISSUE]"
labels: issue
assignees: ''
---
**Describe the issue**
A clear and concise description of what the issue is.
**To Reproduce**
Steps to reproduce the behavior if any:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
Provide `Developer log` if applicable
**Provide information about your station:**
- Weather station type:
- firmware version:
- [ ] Using PWS protocol
- [ ] Using WSLink API
- [ ] Using WSLink proxy Add-on
**Additional context**
Add any other context about the problem here.

View File

@ -1,13 +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
from .utils import long_term_units_in_statistics_meta, migrate_data import homeassistant.helpers.entity_registry as er
from .const import ( from .const import (
API_ID, API_ID,
@ -15,13 +17,18 @@ from .const import (
DEV_DBG, DEV_DBG,
DOMAIN, DOMAIN,
INVALID_CREDENTIALS, INVALID_CREDENTIALS,
SENSORS_TO_LOAD, MIG_FROM,
MIG_TO,
SENSOR_TO_MIGRATE, SENSOR_TO_MIGRATE,
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):
@ -41,16 +48,23 @@ class ConfigOptionsFlowHandler(OptionsFlow):
self.windy_data: dict[str, Any] = {} self.windy_data: dict[str, Any] = {}
self.windy_data_schema = {} self.windy_data_schema = {}
self.user_data: dict[str, str] = {} self.user_data: dict[str, Any] = {}
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):
return self.hass.config_entries.async_get_entry(self.handler) return self.hass.config_entries.async_get_entry(self.handler)
def _get_entry_data(self): async def _get_entry_data(self):
"""Get entry data.""" """Get entry data."""
self.user_data: dict[str, Any] = { self.user_data: dict[str, Any] = {
@ -61,10 +75,10 @@ class ConfigOptionsFlowHandler(OptionsFlow):
} }
self.user_data_schema = { self.user_data_schema = {
vol.Required(API_ID, default=self.user_data[API_ID] or ""): str, vol.Required(API_ID, default=self.user_data.get(API_ID, "")): str,
vol.Required(API_KEY, default=self.user_data[API_KEY] or ""): str, vol.Required(API_KEY, default=self.user_data.get(API_KEY, "")): str,
vol.Optional(WSLINK, default=self.user_data[WSLINK]): bool or False, vol.Optional(WSLINK, default=self.user_data.get(WSLINK, False)): bool,
vol.Optional(DEV_DBG, default=self.user_data[DEV_DBG]): bool or False, vol.Optional(DEV_DBG, default=self.user_data.get(DEV_DBG, False)): bool,
} }
self.sensors: dict[str, Any] = { self.sensors: dict[str, Any] = {
@ -83,7 +97,7 @@ class ConfigOptionsFlowHandler(OptionsFlow):
self.windy_data_schema = { self.windy_data_schema = {
vol.Optional( 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, ): str,
vol.Optional(WINDY_ENABLED, default=self.windy_data[WINDY_ENABLED]): bool vol.Optional(WINDY_ENABLED, default=self.windy_data[WINDY_ENABLED]): bool
or False, or False,
@ -93,12 +107,34 @@ class ConfigOptionsFlowHandler(OptionsFlow):
): bool or False, ): bool or False,
} }
self.migrate_schema = { self.migrate_sensor_select = {
vol.Required(SENSOR_TO_MIGRATE): vol.In( 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, 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."""
@ -110,7 +146,7 @@ class ConfigOptionsFlowHandler(OptionsFlow):
"""Manage basic options - credentials.""" """Manage basic options - credentials."""
errors = {} errors = {}
self._get_entry_data() await self._get_entry_data()
if user_input is None: if user_input is None:
return self.async_show_form( return self.async_show_form(
@ -147,7 +183,7 @@ class ConfigOptionsFlowHandler(OptionsFlow):
"""Manage windy options.""" """Manage windy options."""
errors = {} errors = {}
self._get_entry_data() await self._get_entry_data()
if user_input is None: if user_input is None:
return self.async_show_form( return self.async_show_form(
@ -160,7 +196,7 @@ class ConfigOptionsFlowHandler(OptionsFlow):
errors[WINDY_API_KEY] = "windy_key_required" errors[WINDY_API_KEY] = "windy_key_required"
return self.async_show_form( return self.async_show_form(
step_id="windy", step_id="windy",
data_schema=self.windy_data_schema, data_schema=vol.Schema(self.windy_data_schema),
errors=errors, errors=errors,
) )
@ -175,15 +211,17 @@ class ConfigOptionsFlowHandler(OptionsFlow):
async def async_step_migration(self, user_input=None): async def async_step_migration(self, user_input=None):
"""Migrate sensors.""" """Migrate sensors."""
# hj
errors = {} 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: if user_input is None:
return self.async_show_form( return self.async_show_form(
step_id="migration", step_id="migration",
data_schema=vol.Schema(self.migrate_schema), data_schema=vol.Schema(self.migrate_sensor_select),
errors=errors, errors=errors,
description_placeholders={ description_placeholders={
"migration_status": "-", "migration_status": "-",
@ -191,18 +229,116 @@ class ConfigOptionsFlowHandler(OptionsFlow):
}, },
) )
if user_input.get("trigger_action"): self.selected_sensor = user_input.get(SENSOR_TO_MIGRATE)
# Akce se vykoná po zaškrtnutí
count = await self.hass.async_add_executor_job( return await self.async_step_migration_units()
migrate_data, user_input.get(SENSOR_TO_MIGRATE)
) 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( return self.async_show_form(
step_id="migration", step_id="migration_units",
data_schema=vol.Schema(self.migrate_schema), 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, errors=errors,
description_placeholders={ description_placeholders={
"migration_status": user_input.get(SENSOR_TO_MIGRATE), "migration_sensor": sensor_entry.unit_of_measurement,
"migration_count": count, "migration_stats": sensor_stat.get(self.selected_sensor),
"migration_count": self.count,
}, },
) )

View File

@ -68,6 +68,10 @@ WIND_GUST: Final = "wind_gust"
WIND_DIR: Final = "wind_dir" WIND_DIR: Final = "wind_dir"
WIND_AZIMUT: Final = "wind_azimut" WIND_AZIMUT: Final = "wind_azimut"
RAIN: Final = "rain" RAIN: Final = "rain"
HOURLY_RAIN: Final = "hourly_rain"
WEEKLY_RAIN: Final = "weekly_rain"
MONTHLY_RAIN: Final = "monthly_rain"
YEARLY_RAIN: Final = "yearly_rain"
DAILY_RAIN: Final = "daily_rain" DAILY_RAIN: Final = "daily_rain"
SOLAR_RADIATION: Final = "solar_radiation" SOLAR_RADIATION: Final = "solar_radiation"
INDOOR_TEMP: Final = "indoor_temp" INDOOR_TEMP: Final = "indoor_temp"
@ -129,15 +133,15 @@ REMAP_WSLINK_ITEMS: dict = {
"t234c2cn": CH3_CONNECTION, "t234c2cn": CH3_CONNECTION,
"t1chill": CHILL_INDEX, "t1chill": CHILL_INDEX,
"t1heat": HEAT_INDEX, "t1heat": HEAT_INDEX,
"t1rainhr": HOURLY_RAIN,
"t1rainwy": WEEKLY_RAIN,
"t1rainmth": MONTHLY_RAIN,
"t1rainyr": YEARLY_RAIN,
} }
# TODO: Add more sensors # TODO: Add more sensors
# #
# 'inbat' indoor battery level (1 normal, 0 low) # 'inbat' indoor battery level (1 normal, 0 low)
# 't1rainhr' hourly rain rate in mm
# 't1rainwy' weekly rain rate in mm
# 't1rainmth': monthly rain rate in mm
# 't1rainyr': yearly rain rate in mm
# 't1bat': outdoor battery level (1 normal, 0 low) # 't1bat': outdoor battery level (1 normal, 0 low)
# 't234c1bat': CH2 battery level (1 normal, 0 low) CH2 in integration is CH1 in WSLink # 't234c1bat': CH2 battery level (1 normal, 0 low) CH2 in integration is CH1 in WSLink

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.5.3", "version": "1.6.2",
"zeroconf": [] "zeroconf": []
} }

View File

@ -39,6 +39,10 @@ from .const import (
WIND_GUST, WIND_GUST,
WIND_SPEED, WIND_SPEED,
UnitOfDir, UnitOfDir,
MONTHLY_RAIN,
YEARLY_RAIN,
HOURLY_RAIN,
WEEKLY_RAIN,
) )
from .sensors_common import WeatherSensorEntityDescription from .sensors_common import WeatherSensorEntityDescription
from .utils import wind_dir_to_text from .utils import wind_dir_to_text
@ -139,10 +143,10 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=RAIN, key=RAIN,
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
device_class=SensorDeviceClass.PRECIPITATION, device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
suggested_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, suggested_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
suggested_display_precision=2, suggested_display_precision=2,
icon="mdi:weather-pouring", icon="mdi:weather-pouring",
translation_key=RAIN, translation_key=RAIN,
@ -159,6 +163,50 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
translation_key=DAILY_RAIN, translation_key=DAILY_RAIN,
value_fn=lambda data: cast("float", data), value_fn=lambda data: cast("float", data),
), ),
WeatherSensorEntityDescription(
key=HOURLY_RAIN,
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
suggested_display_precision=2,
icon="mdi:weather-pouring",
translation_key=HOURLY_RAIN,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=WEEKLY_RAIN,
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
suggested_display_precision=2,
icon="mdi:weather-pouring",
translation_key=WEEKLY_RAIN,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=MONTHLY_RAIN,
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
suggested_display_precision=2,
icon="mdi:weather-pouring",
translation_key=MONTHLY_RAIN,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription(
key=YEARLY_RAIN,
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
suggested_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
suggested_display_precision=2,
icon="mdi:weather-pouring",
translation_key=YEARLY_RAIN,
value_fn=lambda data: cast("float", data),
),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=SOLAR_RADIATION, key=SOLAR_RADIATION,
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,

View File

@ -112,6 +112,10 @@
"ch4_humidity": { "name": "Vlhkost sensoru 4" }, "ch4_humidity": { "name": "Vlhkost sensoru 4" },
"heat_index": { "name": "Tepelný index" }, "heat_index": { "name": "Tepelný index" },
"chill_index": { "name": "Pocitová teplota" }, "chill_index": { "name": "Pocitová teplota" },
"hourly_rain": { "name": "Hodinový úhrn srážek" },
"weekly_rain": { "name": "Týdenní úhrn srážek" },
"monthly_rain": { "name": "Měsíční úhrn srážek" },
"yearly_rain": { "name": "Roční úhrn srážek" },
"wind_azimut": { "wind_azimut": {
"name": "Azimut", "name": "Azimut",
"state": { "state": {

View File

@ -35,7 +35,6 @@
}, },
"step": { "step": {
"init": { "init": {
"title": "Configure SWS12500 Integration", "title": "Configure SWS12500 Integration",
"description": "Choose what do you want to configure. If basic access or resending data for Windy site", "description": "Choose what do you want to configure. If basic access or resending data for Windy site",
@ -113,6 +112,10 @@
"ch4_humidity": { "name": "Channel 4 humidity" }, "ch4_humidity": { "name": "Channel 4 humidity" },
"heat_index": { "name": "Apparent temperature" }, "heat_index": { "name": "Apparent temperature" },
"chill_index": { "name": "Wind chill" }, "chill_index": { "name": "Wind chill" },
"hourly_rain": { "name": "Hourly precipitation" },
"weekly_rain": { "name": "Weekly precipitation" },
"monthly_rain": { "name": "Monthly precipitation" },
"yearly_rain": { "name": "Yearly precipitation" },
"wind_azimut": { "wind_azimut": {
"name": "Bearing", "name": "Bearing",
"state": { "state": {

View File

@ -284,7 +284,46 @@ def long_term_units_in_statistics_meta():
return sensor_units return sensor_units
def migrate_data(sensor_id: str | None = None): 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)
updated_rows = 0
if not Path(DATABASE_PATH).exists():
_LOGGER.error("Database file not found: %s", DATABASE_PATH)
return False
conn = sqlite3.connect(DATABASE_PATH)
db = conn.cursor()
try:
_LOGGER.info(sensor_id)
db.execute(
"""
UPDATE statistics_meta
SET unit_of_measurement = 'mm'
WHERE statistic_id = ?
AND unit_of_measurement = 'mm/d';
""",
(sensor_id,),
)
updated_rows = db.rowcount
conn.commit()
_LOGGER.info(
"Data migration completed successfully. Updated rows: %s for %s",
updated_rows,
sensor_id,
)
except sqlite3.Error as e:
_LOGGER.error("Error during data migration: %s", e)
finally:
conn.close()
return updated_rows
def migrate_data_old(sensor_id: str | None = None):
"""Migrate data from mm/d to mm.""" """Migrate data from mm/d to mm."""
updated_rows = 0 updated_rows = 0