Compare commits

..

No commits in common. "main" and "v1.6.1" have entirely different histories.
main ... v1.6.1

13 changed files with 35 additions and 383 deletions

5
.github/FUNDING.yml vendored
View File

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

View File

@ -1,34 +0,0 @@
---
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.

View File

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

View File

@ -1,23 +0,0 @@
---
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.

View File

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

View File

@ -68,10 +68,6 @@ 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"
@ -133,15 +129,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.6.2", "version": "1.5.3",
"zeroconf": [] "zeroconf": []
} }

View File

@ -39,10 +39,6 @@ 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
@ -142,10 +138,10 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=RAIN, key=RAIN,
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
device_class=SensorDeviceClass.PRECIPITATION, device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
suggested_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, suggested_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
suggested_display_precision=2, suggested_display_precision=2,
icon="mdi:weather-pouring", icon="mdi:weather-pouring",
translation_key=RAIN, translation_key=RAIN,
@ -162,50 +158,6 @@ 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,10 +112,6 @@
"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,6 +35,7 @@
}, },
"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",
@ -112,10 +113,6 @@
"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,46 +284,7 @@ def long_term_units_in_statistics_meta():
return sensor_units return sensor_units
async def migrate_data(hass: HomeAssistant, sensor_id: str | None = None) -> bool: def migrate_data(sensor_id: str | None = None):
"""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

View File

@ -1,17 +0,0 @@
"""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"