Compare commits

...

3 Commits

Author SHA1 Message Date
SchiZzA e737fb16d3
Use configured WSLink add-on port for health/status endpoints. 2026-03-23 18:32:02 +01:00
SchiZzA 159d465db5
Improve sensor value parsing and add battery binary sensors.
- Introduce `to_int`/`to_float` helpers to safely handle None/blank payload values.
- Use the converters across weather/wslink sensor descriptions and simplify wind direction handling.
- Add low-battery binary sensor entities and definitions.
2026-03-23 18:21:05 +01:00
SchiZzA 9d5fafa8d0
Add configuration options for WSLink Addon port. 2026-03-23 18:14:46 +01:00
10 changed files with 259 additions and 215 deletions

View File

@ -36,11 +36,7 @@ from py_typecheck import checked, checked_or
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ( from homeassistant.exceptions import ConfigEntryNotReady, InvalidStateError, PlatformNotReady
ConfigEntryNotReady,
InvalidStateError,
PlatformNotReady,
)
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import ( from .const import (
@ -215,9 +211,7 @@ class WeatherDataUpdateCoordinator(DataUpdateCoordinator):
raise HTTPUnauthorized raise HTTPUnauthorized
# Convert raw payload keys to our internal sensor keys (stable identifiers). # Convert raw payload keys to our internal sensor keys (stable identifiers).
remaped_items: dict[str, str] = ( remaped_items: dict[str, str] = remap_wslink_items(data) if _wslink else remap_items(data)
remap_wslink_items(data) if _wslink else remap_items(data)
)
# Auto-discovery: if payload contains keys that are not enabled/loaded yet, # Auto-discovery: if payload contains keys that are not enabled/loaded yet,
# add them to the option list and create entities dynamically. # add them to the option list and create entities dynamically.
@ -274,9 +268,7 @@ class WeatherDataUpdateCoordinator(DataUpdateCoordinator):
# NOTE: Some linters prefer top-level imports. In this case the local import is # NOTE: Some linters prefer top-level imports. In this case the local import is
# intentional and prevents "partially initialized module" errors. # intentional and prevents "partially initialized module" errors.
from .sensor import ( # noqa: PLC0415 (local import is intentional) from .sensor import add_new_sensors # noqa: PLC0415 (local import is intentional)
add_new_sensors,
)
add_new_sensors(self.hass, self.config, newly_discovered) add_new_sensors(self.hass, self.config, newly_discovered)
@ -294,9 +286,7 @@ class WeatherDataUpdateCoordinator(DataUpdateCoordinator):
# to avoid additional background polling tasks. # to avoid additional background polling tasks.
_windy_enabled = checked_or(self.config.options.get(WINDY_ENABLED), bool, False) _windy_enabled = checked_or(self.config.options.get(WINDY_ENABLED), bool, False)
_pocasi_enabled = checked_or( _pocasi_enabled = checked_or(self.config.options.get(POCASI_CZ_ENABLED), bool, False)
self.config.options.get(POCASI_CZ_ENABLED), bool, False
)
if _windy_enabled: if _windy_enabled:
await self.windy.push_data_to_windy(data, _wslink) await self.windy.push_data_to_windy(data, _wslink)
@ -342,38 +332,22 @@ def register_path(
# Register webhooks in HomeAssistant with dispatcher # Register webhooks in HomeAssistant with dispatcher
try: try:
_default_route = hass.http.app.router.add_get( _default_route = hass.http.app.router.add_get(DEFAULT_URL, routes.dispatch, name="_default_route")
DEFAULT_URL, routes.dispatch, name="_default_route" _wslink_post_route = hass.http.app.router.add_post(WSLINK_URL, routes.dispatch, name="_wslink_post_route")
) _wslink_get_route = hass.http.app.router.add_get(WSLINK_URL, routes.dispatch, name="_wslink_get_route")
_wslink_post_route = hass.http.app.router.add_post( _health_route = hass.http.app.router.add_get(HEALTH_URL, routes.dispatch, name="_health_route")
WSLINK_URL, routes.dispatch, name="_wslink_post_route"
)
_wslink_get_route = hass.http.app.router.add_get(
WSLINK_URL, routes.dispatch, name="_wslink_get_route"
)
_health_route = hass.http.app.router.add_get(
HEALTH_URL, routes.dispatch, name="_health_route"
)
# Save initialised routes # Save initialised routes
hass_data["routes"] = routes hass_data["routes"] = routes
except RuntimeError as Ex: except RuntimeError as Ex:
_LOGGER.critical( _LOGGER.critical("Routes cannot be added. Integration will not work as expected. %s", Ex)
"Routes cannot be added. Integration will not work as expected. %s", Ex
)
raise ConfigEntryNotReady from Ex raise ConfigEntryNotReady from Ex
# Finally create internal route dispatcher with provided urls, while we have webhooks registered. # Finally create internal route dispatcher with provided urls, while we have webhooks registered.
routes.add_route( routes.add_route(DEFAULT_URL, _default_route, coordinator.received_data, enabled=not _wslink)
DEFAULT_URL, _default_route, coordinator.received_data, enabled=not _wslink routes.add_route(WSLINK_URL, _wslink_post_route, coordinator.received_data, enabled=_wslink)
) routes.add_route(WSLINK_URL, _wslink_get_route, coordinator.received_data, enabled=_wslink)
routes.add_route(
WSLINK_URL, _wslink_post_route, coordinator.received_data, enabled=_wslink
)
routes.add_route(
WSLINK_URL, _wslink_get_route, coordinator.received_data, enabled=_wslink
)
# Make health route `sticky` so it will not change upon updating options. # Make health route `sticky` so it will not change upon updating options.
routes.add_route( routes.add_route(
HEALTH_URL, HEALTH_URL,
@ -449,9 +423,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if routes: if routes:
_LOGGER.debug("We have routes registered, will try to switch dispatcher.") _LOGGER.debug("We have routes registered, will try to switch dispatcher.")
routes.switch_route( routes.switch_route(coordinator.received_data, DEFAULT_URL if not _wslink else WSLINK_URL)
coordinator.received_data, DEFAULT_URL if not _wslink else WSLINK_URL
)
routes.set_ingress_observer(coordinator_health.record_dispatch) routes.set_ingress_observer(coordinator_health.record_dispatch)
coordinator_health.update_routing(routes) coordinator_health.update_routing(routes)
_LOGGER.debug("%s", routes.show_enabled()) _LOGGER.debug("%s", routes.show_enabled())
@ -487,14 +459,8 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
""" """
if (hass_data := checked(hass.data.get(DOMAIN), dict[str, Any])) is not None: if (hass_data := checked(hass.data.get(DOMAIN), dict[str, Any])) is not None:
if ( if (entry_data := checked(hass_data.get(entry.entry_id), dict[str, Any])) is not None:
entry_data := checked(hass_data.get(entry.entry_id), dict[str, Any]) if (old_options := checked(entry_data.get(ENTRY_LAST_OPTIONS), dict[str, Any])) is not None:
) is not None:
if (
old_options := checked(
entry_data.get(ENTRY_LAST_OPTIONS), dict[str, Any]
)
) is not None:
new_options = dict(entry.options) new_options = dict(entry.options)
changed_keys = { changed_keys = {
@ -507,9 +473,7 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
entry_data[ENTRY_LAST_OPTIONS] = new_options entry_data[ENTRY_LAST_OPTIONS] = new_options
if changed_keys == {SENSORS_TO_LOAD}: if changed_keys == {SENSORS_TO_LOAD}:
_LOGGER.debug( _LOGGER.debug("Options updated (%s); skipping reload.", SENSORS_TO_LOAD)
"Options updated (%s); skipping reload.", SENSORS_TO_LOAD
)
return return
else: else:
# No/invalid snapshot: store current options for next comparison. # No/invalid snapshot: store current options for next comparison.

View File

@ -0,0 +1,53 @@
"""Battery binary sensor entities."""
from __future__ import annotations
from typing import Any
from py_typecheck import checked_or
from homeassistant.components.binary_sensor import BinarySensorEntity, BinarySensorEntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
class BatteryBinarySensor( # pyright: ignore[reportIncompatibleVariableOverride]
CoordinatorEntity, BinarySensorEntity
):
"""Represent a low-battery binary sensor.
Station payload uses:
- ``0`` => low battery (binary sensor is ``on``)
- ``1`` => battery OK (binary sensor is ``off``)
"""
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(
self,
coordinator: Any,
description: BinarySensorEntityDescription,
) -> None:
"""Initialize the battery binary sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{description.key}_battery"
@property
def is_on(self) -> bool | None: # pyright: ignore[reportIncompatibleVariableOverride]
"""Return low-battery state.
``True`` means low battery for ``BinarySensorDeviceClass.BATTERY``.
"""
data = checked_or(self.coordinator.data, dict[str, Any], {})
raw: Any = data.get(self.entity_description.key)
if raw is None or raw == "":
return None
try:
value = int(raw)
except (TypeError, ValueError):
return None
return value == 0

View File

@ -0,0 +1,24 @@
"""Battery sensors."""
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntityDescription,
)
BATTERY_BINARY_SENSORS: tuple[BinarySensorEntityDescription, ...] = (
BinarySensorEntityDescription(
key="outside_battery",
translation_key="outside_battery",
device_class=BinarySensorDeviceClass.BATTERY,
),
BinarySensorEntityDescription(
key="indoor_battery",
translation_key="indoor_battery",
device_class=BinarySensorDeviceClass.BATTERY,
),
BinarySensorEntityDescription(
key="ch2_battery",
translation_key="ch2_battery",
device_class=BinarySensorDeviceClass.BATTERY,
),
)

View File

@ -6,12 +6,7 @@ from typing import Any
import voluptuous as vol import voluptuous as vol
from yarl import URL from yarl import URL
from homeassistant.config_entries import ( from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult, OptionsFlow
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.network import get_url from homeassistant.helpers.network import get_url
@ -23,7 +18,6 @@ from .const import (
DOMAIN, DOMAIN,
ECOWITT_ENABLED, ECOWITT_ENABLED,
ECOWITT_WEBHOOK_ID, ECOWITT_WEBHOOK_ID,
# HEALTH_BEARER_TOKEN,
INVALID_CREDENTIALS, INVALID_CREDENTIALS,
POCASI_CZ_API_ID, POCASI_CZ_API_ID,
POCASI_CZ_API_KEY, POCASI_CZ_API_KEY,
@ -37,6 +31,7 @@ from .const import (
WINDY_STATION_ID, WINDY_STATION_ID,
WINDY_STATION_PW, WINDY_STATION_PW,
WSLINK, WSLINK,
WSLINK_ADDON_PORT,
) )
@ -65,6 +60,8 @@ class ConfigOptionsFlowHandler(OptionsFlow):
self.pocasi_cz_schema = {} self.pocasi_cz_schema = {}
self.ecowitt: dict[str, Any] = {} self.ecowitt: dict[str, Any] = {}
self.ecowitt_schema = {} self.ecowitt_schema = {}
self.wslink_addon_port: dict[str, int] = {}
self.wslink_addod_schema = {}
async def _get_entry_data(self): async def _get_entry_data(self):
"""Get entry data.""" """Get entry data."""
@ -94,22 +91,17 @@ class ConfigOptionsFlowHandler(OptionsFlow):
self.windy_data = { self.windy_data = {
WINDY_STATION_ID: self.config_entry.options.get(WINDY_STATION_ID, ""), WINDY_STATION_ID: self.config_entry.options.get(WINDY_STATION_ID, ""),
WINDY_STATION_PW: self.config_entry.options.get(WINDY_STATION_PW, ""), WINDY_STATION_PW: self.config_entry.options.get(WINDY_STATION_PW, ""),
WINDY_LOGGER_ENABLED: self.config_entry.options.get( WINDY_LOGGER_ENABLED: self.config_entry.options.get(WINDY_LOGGER_ENABLED, False),
WINDY_LOGGER_ENABLED, False
),
WINDY_ENABLED: self.config_entry.options.get(WINDY_ENABLED, False), WINDY_ENABLED: self.config_entry.options.get(WINDY_ENABLED, False),
} }
self.windy_data_schema = { self.windy_data_schema = {
vol.Optional( vol.Optional(WINDY_STATION_ID, default=self.windy_data.get(WINDY_STATION_ID, "")): str,
WINDY_STATION_ID, default=self.windy_data.get(WINDY_STATION_ID, "")
): str,
vol.Optional( vol.Optional(
WINDY_STATION_PW, WINDY_STATION_PW,
default=self.windy_data.get(WINDY_STATION_PW, ""), default=self.windy_data.get(WINDY_STATION_PW, ""),
): 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,
vol.Optional( vol.Optional(
WINDY_LOGGER_ENABLED, WINDY_LOGGER_ENABLED,
default=self.windy_data[WINDY_LOGGER_ENABLED], default=self.windy_data[WINDY_LOGGER_ENABLED],
@ -120,28 +112,18 @@ class ConfigOptionsFlowHandler(OptionsFlow):
POCASI_CZ_API_ID: self.config_entry.options.get(POCASI_CZ_API_ID, ""), POCASI_CZ_API_ID: self.config_entry.options.get(POCASI_CZ_API_ID, ""),
POCASI_CZ_API_KEY: self.config_entry.options.get(POCASI_CZ_API_KEY, ""), POCASI_CZ_API_KEY: self.config_entry.options.get(POCASI_CZ_API_KEY, ""),
POCASI_CZ_ENABLED: self.config_entry.options.get(POCASI_CZ_ENABLED, False), POCASI_CZ_ENABLED: self.config_entry.options.get(POCASI_CZ_ENABLED, False),
POCASI_CZ_LOGGER_ENABLED: self.config_entry.options.get( POCASI_CZ_LOGGER_ENABLED: self.config_entry.options.get(POCASI_CZ_LOGGER_ENABLED, False),
POCASI_CZ_LOGGER_ENABLED, False POCASI_CZ_SEND_INTERVAL: self.config_entry.options.get(POCASI_CZ_SEND_INTERVAL, 30),
),
POCASI_CZ_SEND_INTERVAL: self.config_entry.options.get(
POCASI_CZ_SEND_INTERVAL, 30
),
} }
self.pocasi_cz_schema = { self.pocasi_cz_schema = {
vol.Required( vol.Required(POCASI_CZ_API_ID, default=self.pocasi_cz.get(POCASI_CZ_API_ID)): str,
POCASI_CZ_API_ID, default=self.pocasi_cz.get(POCASI_CZ_API_ID) vol.Required(POCASI_CZ_API_KEY, default=self.pocasi_cz.get(POCASI_CZ_API_KEY)): str,
): str,
vol.Required(
POCASI_CZ_API_KEY, default=self.pocasi_cz.get(POCASI_CZ_API_KEY)
): str,
vol.Required( vol.Required(
POCASI_CZ_SEND_INTERVAL, POCASI_CZ_SEND_INTERVAL,
default=self.pocasi_cz.get(POCASI_CZ_SEND_INTERVAL), default=self.pocasi_cz.get(POCASI_CZ_SEND_INTERVAL),
): int, ): int,
vol.Optional( vol.Optional(POCASI_CZ_ENABLED, default=self.pocasi_cz.get(POCASI_CZ_ENABLED)): bool,
POCASI_CZ_ENABLED, default=self.pocasi_cz.get(POCASI_CZ_ENABLED)
): bool,
vol.Optional( vol.Optional(
POCASI_CZ_LOGGER_ENABLED, POCASI_CZ_LOGGER_ENABLED,
default=self.pocasi_cz.get(POCASI_CZ_LOGGER_ENABLED), default=self.pocasi_cz.get(POCASI_CZ_LOGGER_ENABLED),
@ -153,11 +135,13 @@ class ConfigOptionsFlowHandler(OptionsFlow):
ECOWITT_ENABLED: self.config_entry.options.get(ECOWITT_ENABLED, False), ECOWITT_ENABLED: self.config_entry.options.get(ECOWITT_ENABLED, False),
} }
self.wslink_addon_port = {WSLINK_ADDON_PORT: self.config_entry.options.get(WSLINK_ADDON_PORT, 443)}
async def async_step_init(self, user_input: dict[str, Any] = {}): async def async_step_init(self, user_input: dict[str, Any] = {}):
"""Manage the options - show menu first.""" """Manage the options - show menu first."""
_ = user_input _ = user_input
return self.async_show_menu( return self.async_show_menu(
step_id="init", menu_options=["basic", "ecowitt", "windy", "pocasi"] step_id="init", menu_options=["basic", "wslink_port_setup", "ecowitt", "windy", "pocasi"]
) )
async def async_step_basic(self, user_input: Any = None): async def async_step_basic(self, user_input: Any = None):
@ -294,6 +278,28 @@ class ConfigOptionsFlowHandler(OptionsFlow):
user_input = self.retain_data(user_input) user_input = self.retain_data(user_input)
return self.async_create_entry(title=DOMAIN, data=user_input) return self.async_create_entry(title=DOMAIN, data=user_input)
async def async_step_wslink_port_setup(self, user_input: Any = None) -> ConfigFlowResult:
"""WSLink Addon port setup."""
errors: dict[str, str] = {}
await self._get_entry_data()
if not (port := self.wslink_addon_port.get(WSLINK_ADDON_PORT)):
port = 433
wslink_port_schema = {
vol.Required(WSLINK_ADDON_PORT, default=port): int,
}
if user_input is None:
return self.async_show_form(
step_id="wslink_port_setup",
data_schema=vol.Schema(wslink_port_schema),
errors=errors,
)
user_input = self.retain_data(user_input)
return self.async_create_entry(title=DOMAIN, data=user_input)
def retain_data(self, data: dict[str, Any]) -> dict[str, Any]: def retain_data(self, data: dict[str, Any]) -> dict[str, Any]:
"""Retain user_data.""" """Retain user_data."""
@ -303,6 +309,7 @@ class ConfigOptionsFlowHandler(OptionsFlow):
**self.pocasi_cz, **self.pocasi_cz,
**self.sensors, **self.sensors,
**self.ecowitt, **self.ecowitt,
**self.wslink_addon_port,
**dict(data), **dict(data),
} }
@ -339,9 +346,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
elif user_input[API_KEY] == user_input[API_ID]: elif user_input[API_KEY] == user_input[API_ID]:
errors["base"] = "valid_credentials_match" errors["base"] = "valid_credentials_match"
else: else:
return self.async_create_entry( return self.async_create_entry(title=DOMAIN, data=user_input, options=user_input)
title=DOMAIN, data=user_input, options=user_input
)
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",

View File

@ -143,6 +143,7 @@ POCASI_CZ_SEND_MINIMUM: Final = 12 # minimal time to resend data
WSLINK: Final = "wslink" WSLINK: Final = "wslink"
WINDY_MAX_RETRIES: Final = 3 WINDY_MAX_RETRIES: Final = 3
WSLINK_ADDON_PORT: Final = "WSLINK_ADDON_PORT"
__all__ = [ __all__ = [
"DOMAIN", "DOMAIN",
@ -235,28 +236,20 @@ POCASI_CZ_API_ID = "POCASI_CZ_API_ID"
POCASI_CZ_SEND_INTERVAL = "POCASI_SEND_INTERVAL" POCASI_CZ_SEND_INTERVAL = "POCASI_SEND_INTERVAL"
POCASI_CZ_ENABLED = "pocasi_enabled_chcekbox" POCASI_CZ_ENABLED = "pocasi_enabled_chcekbox"
POCASI_CZ_LOGGER_ENABLED = "pocasi_logger_checkbox" POCASI_CZ_LOGGER_ENABLED = "pocasi_logger_checkbox"
POCASI_INVALID_KEY: Final = ( POCASI_INVALID_KEY: Final = "Pocasi Meteo refused to accept data. Invalid ID/Key combination?"
"Pocasi Meteo refused to accept data. Invalid ID/Key combination?"
)
POCASI_CZ_SUCCESS: Final = "Successfully sent data to Pocasi Meteo" POCASI_CZ_SUCCESS: Final = "Successfully sent data to Pocasi Meteo"
POCASI_CZ_UNEXPECTED: Final = ( POCASI_CZ_UNEXPECTED: Final = "Pocasti Meteo responded unexpectedly 3 times in row. Resendig is now disabled!"
"Pocasti Meteo responded unexpectedly 3 times in row. Resendig is now disabled!"
)
WINDY_STATION_ID = "WINDY_STATION_ID" WINDY_STATION_ID = "WINDY_STATION_ID"
WINDY_STATION_PW = "WINDY_STATION_PWD" WINDY_STATION_PW = "WINDY_STATION_PWD"
WINDY_ENABLED: Final = "windy_enabled_checkbox" WINDY_ENABLED: Final = "windy_enabled_checkbox"
WINDY_LOGGER_ENABLED: Final = "windy_logger_checkbox" WINDY_LOGGER_ENABLED: Final = "windy_logger_checkbox"
WINDY_NOT_INSERTED: Final = ( WINDY_NOT_INSERTED: Final = "Windy responded with 400 error. Invalid ID/password combination?"
"Windy responded with 400 error. Invalid ID/password combination?" WINDY_INVALID_KEY: Final = (
) "Windy API KEY is invalid. Send data to Windy is now disabled. Check your API KEY and try again."
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"
)
WINDY_UNEXPECTED: Final = (
"Windy responded unexpectedly 3 times in a row. Send to Windy is now disabled!"
) )
WINDY_SUCCESS: Final = "Windy successfully sent data and data was successfully inserted by Windy API"
WINDY_UNEXPECTED: Final = "Windy responded unexpectedly 3 times in a row. Send to Windy is now disabled!"
PURGE_DATA_POCAS: Final = [ PURGE_DATA_POCAS: Final = [

View File

@ -40,6 +40,7 @@ from .const import (
POCASI_CZ_ENABLED, POCASI_CZ_ENABLED,
WINDY_ENABLED, WINDY_ENABLED,
WSLINK, WSLINK,
WSLINK_ADDON_PORT,
WSLINK_URL, WSLINK_URL,
) )
from .data import ENTRY_HEALTH_DATA from .data import ENTRY_HEALTH_DATA
@ -78,9 +79,7 @@ def _empty_forwarding_state(enabled: bool) -> dict[str, Any]:
def _default_health_data(config: ConfigEntry) -> dict[str, Any]: def _default_health_data(config: ConfigEntry) -> dict[str, Any]:
"""Build the default health/debug payload for this config entry.""" """Build the default health/debug payload for this config entry."""
configured_protocol = _protocol_name( configured_protocol = _protocol_name(checked_or(config.options.get(WSLINK), bool, False))
checked_or(config.options.get(WSLINK), bool, False)
)
return { return {
"integration_status": f"online_{configured_protocol}", "integration_status": f"online_{configured_protocol}",
"configured_protocol": configured_protocol, "configured_protocol": configured_protocol,
@ -117,12 +116,8 @@ def _default_health_data(config: ConfigEntry) -> dict[str, Any]:
"reason": "no_data", "reason": "no_data",
}, },
"forwarding": { "forwarding": {
"windy": _empty_forwarding_state( "windy": _empty_forwarding_state(checked_or(config.options.get(WINDY_ENABLED), bool, False)),
checked_or(config.options.get(WINDY_ENABLED), bool, False) "pocasi": _empty_forwarding_state(checked_or(config.options.get(POCASI_CZ_ENABLED), bool, False)),
),
"pocasi": _empty_forwarding_state(
checked_or(config.options.get(POCASI_CZ_ENABLED), bool, False)
),
}, },
} }
@ -190,9 +185,7 @@ class HealthCoordinator(DataUpdateCoordinator):
data["integration_status"] = integration_status data["integration_status"] = integration_status
data["active_protocol"] = ( data["active_protocol"] = (
last_protocol last_protocol if accepted and last_protocol in {"wu", "wslink"} else configured_protocol
if accepted and last_protocol in {"wu", "wslink"}
else configured_protocol
) )
async def _async_update_data(self) -> dict[str, Any]: async def _async_update_data(self) -> dict[str, Any]:
@ -201,8 +194,10 @@ class HealthCoordinator(DataUpdateCoordinator):
url = get_url(self.hass) url = get_url(self.hass)
ip = await async_get_source_ip(self.hass) ip = await async_get_source_ip(self.hass)
health_url = f"https://{ip}/healthz" port = checked_or(self.config_entry.options.get(WSLINK_ADDON_PORT), int, 443)
info_url = f"https://{ip}/status/internal"
health_url = f"https://{ip}:{port}/healthz"
info_url = f"https://{ip}:{port}/status/internal"
data = deepcopy(self.data) data = deepcopy(self.data)
addon = data["addon"] addon = data["addon"]
@ -245,9 +240,7 @@ class HealthCoordinator(DataUpdateCoordinator):
def update_routing(self, routes: Routes | None) -> None: def update_routing(self, routes: Routes | None) -> None:
"""Store the currently enabled routes for diagnostics.""" """Store the currently enabled routes for diagnostics."""
data = deepcopy(self.data) data = deepcopy(self.data)
data["configured_protocol"] = _protocol_name( data["configured_protocol"] = _protocol_name(checked_or(self.config.options.get(WSLINK), bool, False))
checked_or(self.config.options.get(WSLINK), bool, False)
)
if routes is not None: if routes is not None:
data["routes"] = { data["routes"] = {
"wu_enabled": routes.path_enabled(DEFAULT_URL), "wu_enabled": routes.path_enabled(DEFAULT_URL),
@ -258,9 +251,7 @@ class HealthCoordinator(DataUpdateCoordinator):
self._refresh_summary(data) self._refresh_summary(data)
self._commit(data) self._commit(data)
def record_dispatch( def record_dispatch(self, request: aiohttp.web.Request, route_enabled: bool, reason: str | None) -> None:
self, request: aiohttp.web.Request, route_enabled: bool, reason: str | None
) -> None:
"""Record every ingress observed by the dispatcher. """Record every ingress observed by the dispatcher.
This runs before the actual webhook handler. It lets diagnostics answer: This runs before the actual webhook handler. It lets diagnostics answer:

View File

@ -1,7 +1,5 @@
"""Sensor entities for the SWS12500 integration for old endpoint.""" """Sensor entities for the SWS12500 integration for old endpoint."""
from typing import cast
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.const import ( from homeassistant.const import (
DEGREE, DEGREE,
@ -41,7 +39,7 @@ from .const import (
UnitOfDir, UnitOfDir,
) )
from .sensors_common import WeatherSensorEntityDescription from .sensors_common import WeatherSensorEntityDescription
from .utils import chill_index, heat_index, wind_dir_to_text from .utils import chill_index, heat_index, to_float, to_int, wind_dir_to_text
SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = ( SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
@ -51,7 +49,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
icon="mdi:thermometer", icon="mdi:thermometer",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
translation_key=INDOOR_TEMP, translation_key=INDOOR_TEMP,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=INDOOR_HUMIDITY, key=INDOOR_HUMIDITY,
@ -60,7 +58,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
icon="mdi:thermometer", icon="mdi:thermometer",
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
translation_key=INDOOR_HUMIDITY, translation_key=INDOOR_HUMIDITY,
value_fn=lambda data: cast("int", data), value_fn=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=OUTSIDE_TEMP, key=OUTSIDE_TEMP,
@ -69,7 +67,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
icon="mdi:thermometer", icon="mdi:thermometer",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
translation_key=OUTSIDE_TEMP, translation_key=OUTSIDE_TEMP,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=OUTSIDE_HUMIDITY, key=OUTSIDE_HUMIDITY,
@ -78,7 +76,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
icon="mdi:thermometer", icon="mdi:thermometer",
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
translation_key=OUTSIDE_HUMIDITY, translation_key=OUTSIDE_HUMIDITY,
value_fn=lambda data: cast("int", data), value_fn=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=DEW_POINT, key=DEW_POINT,
@ -87,7 +85,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
icon="mdi:thermometer-lines", icon="mdi:thermometer-lines",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
translation_key=DEW_POINT, translation_key=DEW_POINT,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=BARO_PRESSURE, key=BARO_PRESSURE,
@ -97,7 +95,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE, device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
suggested_unit_of_measurement=UnitOfPressure.HPA, suggested_unit_of_measurement=UnitOfPressure.HPA,
translation_key=BARO_PRESSURE, translation_key=BARO_PRESSURE,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=WIND_SPEED, key=WIND_SPEED,
@ -107,7 +105,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
suggested_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, suggested_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
icon="mdi:weather-windy", icon="mdi:weather-windy",
translation_key=WIND_SPEED, translation_key=WIND_SPEED,
value_fn=lambda data: cast("int", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=WIND_GUST, key=WIND_GUST,
@ -117,7 +115,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
suggested_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, suggested_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
icon="mdi:windsock", icon="mdi:windsock",
translation_key=WIND_GUST, translation_key=WIND_GUST,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=WIND_DIR, key=WIND_DIR,
@ -127,15 +125,12 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
suggested_display_precision=None, suggested_display_precision=None,
icon="mdi:sign-direction", icon="mdi:sign-direction",
translation_key=WIND_DIR, translation_key=WIND_DIR,
value_fn=lambda data: cast("int", data), value_fn=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=WIND_AZIMUT, key=WIND_AZIMUT,
icon="mdi:sign-direction", icon="mdi:sign-direction",
value_fn=lambda data: cast("str", wind_dir_to_text(data)), value_from_data_fn=lambda dir: wind_dir_to_text(dir.get(WIND_DIR, 0.0)),
value_from_data_fn=lambda data: cast(
"str", wind_dir_to_text(cast("float", data.get(WIND_DIR) or 0.0))
),
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
options=[e.value for e in UnitOfDir], options=[e.value for e in UnitOfDir],
translation_key=WIND_AZIMUT, translation_key=WIND_AZIMUT,
@ -149,7 +144,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
suggested_display_precision=2, suggested_display_precision=2,
icon="mdi:weather-pouring", icon="mdi:weather-pouring",
translation_key=RAIN, translation_key=RAIN,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=DAILY_RAIN, key=DAILY_RAIN,
@ -160,7 +155,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
suggested_display_precision=2, suggested_display_precision=2,
icon="mdi:weather-pouring", icon="mdi:weather-pouring",
translation_key=DAILY_RAIN, translation_key=DAILY_RAIN,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=SOLAR_RADIATION, key=SOLAR_RADIATION,
@ -169,7 +164,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.IRRADIANCE, device_class=SensorDeviceClass.IRRADIANCE,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=SOLAR_RADIATION, translation_key=SOLAR_RADIATION,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=UV, key=UV,
@ -178,7 +173,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
native_unit_of_measurement=UV_INDEX, native_unit_of_measurement=UV_INDEX,
icon="mdi:sunglasses", icon="mdi:sunglasses",
translation_key=UV, translation_key=UV,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH2_TEMP, key=CH2_TEMP,
@ -188,7 +183,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS, suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=CH2_TEMP, translation_key=CH2_TEMP,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH2_HUMIDITY, key=CH2_HUMIDITY,
@ -197,7 +192,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=CH2_HUMIDITY, translation_key=CH2_HUMIDITY,
value_fn=lambda data: cast("int", data), value_fn=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH3_TEMP, key=CH3_TEMP,
@ -207,7 +202,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
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=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH3_HUMIDITY, key=CH3_HUMIDITY,
@ -216,7 +211,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
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=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH4_TEMP, key=CH4_TEMP,
@ -226,7 +221,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS, suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=CH4_TEMP, translation_key=CH4_TEMP,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH4_HUMIDITY, key=CH4_HUMIDITY,
@ -235,7 +230,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=CH4_HUMIDITY, translation_key=CH4_HUMIDITY,
value_fn=lambda data: cast("int", data), value_fn=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=HEAT_INDEX, key=HEAT_INDEX,
@ -246,7 +241,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
suggested_display_precision=2, suggested_display_precision=2,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=HEAT_INDEX, translation_key=HEAT_INDEX,
value_fn=lambda data: cast("int", data), value_fn=to_int,
value_from_data_fn=heat_index, value_from_data_fn=heat_index,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
@ -258,7 +253,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
suggested_display_precision=2, suggested_display_precision=2,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=CHILL_INDEX, translation_key=CHILL_INDEX,
value_fn=lambda data: cast("int", data), value_fn=to_int,
value_from_data_fn=chill_index, value_from_data_fn=chill_index,
), ),
) )

View File

@ -1,7 +1,5 @@
"""Sensor entities for the SWS12500 integration for old endpoint.""" """Sensor entities for the SWS12500 integration for old endpoint."""
from typing import cast
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.const import ( from homeassistant.const import (
DEGREE, DEGREE,
@ -64,7 +62,7 @@ from .const import (
UnitOfDir, UnitOfDir,
) )
from .sensors_common import WeatherSensorEntityDescription from .sensors_common import WeatherSensorEntityDescription
from .utils import battery_level, wind_dir_to_text from .utils import battery_level, to_float, to_int, wind_dir_to_text
SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = ( SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
@ -74,7 +72,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
icon="mdi:thermometer", icon="mdi:thermometer",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
translation_key=INDOOR_TEMP, translation_key=INDOOR_TEMP,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=INDOOR_HUMIDITY, key=INDOOR_HUMIDITY,
@ -83,7 +81,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
icon="mdi:thermometer", icon="mdi:thermometer",
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
translation_key=INDOOR_HUMIDITY, translation_key=INDOOR_HUMIDITY,
value_fn=lambda data: cast("int", data), value_fn=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=OUTSIDE_TEMP, key=OUTSIDE_TEMP,
@ -92,7 +90,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
icon="mdi:thermometer", icon="mdi:thermometer",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
translation_key=OUTSIDE_TEMP, translation_key=OUTSIDE_TEMP,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=OUTSIDE_HUMIDITY, key=OUTSIDE_HUMIDITY,
@ -101,7 +99,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
icon="mdi:thermometer", icon="mdi:thermometer",
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
translation_key=OUTSIDE_HUMIDITY, translation_key=OUTSIDE_HUMIDITY,
value_fn=lambda data: cast("int", data), value_fn=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=DEW_POINT, key=DEW_POINT,
@ -110,7 +108,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
icon="mdi:thermometer-lines", icon="mdi:thermometer-lines",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
translation_key=DEW_POINT, translation_key=DEW_POINT,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=BARO_PRESSURE, key=BARO_PRESSURE,
@ -120,7 +118,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE, device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
suggested_unit_of_measurement=UnitOfPressure.HPA, suggested_unit_of_measurement=UnitOfPressure.HPA,
translation_key=BARO_PRESSURE, translation_key=BARO_PRESSURE,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=WIND_SPEED, key=WIND_SPEED,
@ -130,7 +128,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, suggested_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
icon="mdi:weather-windy", icon="mdi:weather-windy",
translation_key=WIND_SPEED, translation_key=WIND_SPEED,
value_fn=lambda data: cast("int", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=WIND_GUST, key=WIND_GUST,
@ -140,7 +138,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, suggested_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
icon="mdi:windsock", icon="mdi:windsock",
translation_key=WIND_GUST, translation_key=WIND_GUST,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=WIND_DIR, key=WIND_DIR,
@ -150,15 +148,12 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_display_precision=None, suggested_display_precision=None,
icon="mdi:sign-direction", icon="mdi:sign-direction",
translation_key=WIND_DIR, translation_key=WIND_DIR,
value_fn=lambda data: cast("int", data), value_fn=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=WIND_AZIMUT, key=WIND_AZIMUT,
icon="mdi:sign-direction", icon="mdi:sign-direction",
value_fn=lambda data: cast("str", wind_dir_to_text(data)), value_from_data_fn=lambda dir: wind_dir_to_text(dir.get(WIND_DIR, 0.0)),
value_from_data_fn=lambda data: cast(
"str", wind_dir_to_text(cast("float", data.get(WIND_DIR) or 0.0))
),
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
options=[e.value for e in UnitOfDir], options=[e.value for e in UnitOfDir],
translation_key=WIND_AZIMUT, translation_key=WIND_AZIMUT,
@ -172,7 +167,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_display_precision=2, suggested_display_precision=2,
icon="mdi:weather-pouring", icon="mdi:weather-pouring",
translation_key=RAIN, translation_key=RAIN,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=DAILY_RAIN, key=DAILY_RAIN,
@ -183,7 +178,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_display_precision=2, suggested_display_precision=2,
icon="mdi:weather-pouring", icon="mdi:weather-pouring",
translation_key=DAILY_RAIN, translation_key=DAILY_RAIN,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=HOURLY_RAIN, key=HOURLY_RAIN,
@ -194,7 +189,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_display_precision=2, suggested_display_precision=2,
icon="mdi:weather-pouring", icon="mdi:weather-pouring",
translation_key=HOURLY_RAIN, translation_key=HOURLY_RAIN,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=WEEKLY_RAIN, key=WEEKLY_RAIN,
@ -205,7 +200,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_display_precision=2, suggested_display_precision=2,
icon="mdi:weather-pouring", icon="mdi:weather-pouring",
translation_key=WEEKLY_RAIN, translation_key=WEEKLY_RAIN,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=MONTHLY_RAIN, key=MONTHLY_RAIN,
@ -216,7 +211,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_display_precision=2, suggested_display_precision=2,
icon="mdi:weather-pouring", icon="mdi:weather-pouring",
translation_key=MONTHLY_RAIN, translation_key=MONTHLY_RAIN,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=YEARLY_RAIN, key=YEARLY_RAIN,
@ -227,7 +222,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_display_precision=2, suggested_display_precision=2,
icon="mdi:weather-pouring", icon="mdi:weather-pouring",
translation_key=YEARLY_RAIN, translation_key=YEARLY_RAIN,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=SOLAR_RADIATION, key=SOLAR_RADIATION,
@ -236,7 +231,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.IRRADIANCE, device_class=SensorDeviceClass.IRRADIANCE,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=SOLAR_RADIATION, translation_key=SOLAR_RADIATION,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=UV, key=UV,
@ -245,7 +240,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
native_unit_of_measurement=UV_INDEX, native_unit_of_measurement=UV_INDEX,
icon="mdi:sunglasses", icon="mdi:sunglasses",
translation_key=UV, translation_key=UV,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH2_TEMP, key=CH2_TEMP,
@ -255,7 +250,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS, suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=CH2_TEMP, translation_key=CH2_TEMP,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH2_HUMIDITY, key=CH2_HUMIDITY,
@ -264,7 +259,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=CH2_HUMIDITY, translation_key=CH2_HUMIDITY,
value_fn=lambda data: cast("int", data), value_fn=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH3_TEMP, key=CH3_TEMP,
@ -274,7 +269,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
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=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH3_HUMIDITY, key=CH3_HUMIDITY,
@ -283,7 +278,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
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=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH4_TEMP, key=CH4_TEMP,
@ -293,7 +288,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS, suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=CH4_TEMP, translation_key=CH4_TEMP,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH4_HUMIDITY, key=CH4_HUMIDITY,
@ -302,7 +297,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=CH4_HUMIDITY, translation_key=CH4_HUMIDITY,
value_fn=lambda data: cast("int", data), value_fn=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH5_TEMP, key=CH5_TEMP,
@ -312,7 +307,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS, suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=CH5_TEMP, translation_key=CH5_TEMP,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH5_HUMIDITY, key=CH5_HUMIDITY,
@ -321,7 +316,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=CH5_HUMIDITY, translation_key=CH5_HUMIDITY,
value_fn=lambda data: cast("int", data), value_fn=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH6_TEMP, key=CH6_TEMP,
@ -331,7 +326,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS, suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=CH6_TEMP, translation_key=CH6_TEMP,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH6_HUMIDITY, key=CH6_HUMIDITY,
@ -340,7 +335,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=CH6_HUMIDITY, translation_key=CH6_HUMIDITY,
value_fn=lambda data: cast("int", data), value_fn=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH7_TEMP, key=CH7_TEMP,
@ -350,7 +345,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS, suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=CH7_TEMP, translation_key=CH7_TEMP,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH7_HUMIDITY, key=CH7_HUMIDITY,
@ -359,7 +354,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=CH7_HUMIDITY, translation_key=CH7_HUMIDITY,
value_fn=lambda data: cast("int", data), value_fn=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH8_TEMP, key=CH8_TEMP,
@ -369,7 +364,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS, suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=CH8_TEMP, translation_key=CH8_TEMP,
value_fn=lambda data: cast("float", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH8_HUMIDITY, key=CH8_HUMIDITY,
@ -378,34 +373,28 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=CH8_HUMIDITY, translation_key=CH8_HUMIDITY,
value_fn=lambda data: cast("int", data), value_fn=to_int,
),
WeatherSensorEntityDescription(
key=HEAT_INDEX,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.ENUM,
value_fn=lambda data: data,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH3_BATTERY, key=CH3_BATTERY,
translation_key=CH3_BATTERY, translation_key=CH3_BATTERY,
icon="mdi:battery-unknown", icon="mdi:battery-unknown",
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
value_fn=lambda data: data, value_fn=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH4_BATTERY, key=CH4_BATTERY,
translation_key=CH4_BATTERY, translation_key=CH4_BATTERY,
icon="mdi:battery-unknown", icon="mdi:battery-unknown",
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
value_fn=lambda data: data, value_fn=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH5_BATTERY, key=CH5_BATTERY,
translation_key=CH5_BATTERY, translation_key=CH5_BATTERY,
icon="mdi:battery-unknown", icon="mdi:battery-unknown",
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
value_fn=lambda data: data, value_fn=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH6_BATTERY, key=CH6_BATTERY,
@ -419,14 +408,14 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
translation_key=CH7_BATTERY, translation_key=CH7_BATTERY,
icon="mdi:battery-unknown", icon="mdi:battery-unknown",
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
value_fn=lambda data: data, value_fn=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CH8_BATTERY, key=CH8_BATTERY,
translation_key=CH8_BATTERY, translation_key=CH8_BATTERY,
icon="mdi:battery-unknown", icon="mdi:battery-unknown",
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
value_fn=lambda data: data, value_fn=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=HEAT_INDEX, key=HEAT_INDEX,
@ -437,7 +426,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_display_precision=2, suggested_display_precision=2,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=HEAT_INDEX, translation_key=HEAT_INDEX,
value_fn=lambda data: cast("int", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=CHILL_INDEX, key=CHILL_INDEX,
@ -448,7 +437,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
suggested_display_precision=2, suggested_display_precision=2,
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
translation_key=CHILL_INDEX, translation_key=CHILL_INDEX,
value_fn=lambda data: cast("int", data), value_fn=to_float,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=OUTSIDE_BATTERY, key=OUTSIDE_BATTERY,
@ -473,12 +462,8 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=INDOOR_BATTERY, key=INDOOR_BATTERY,
translation_key=INDOOR_BATTERY, translation_key=INDOOR_BATTERY,
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.BATTERY,
options=[e.value for e in UnitOfBat], value_fn=to_int,
value_fn=None,
value_from_data_fn=lambda data: (
battery_level(data.get(INDOOR_BATTERY, None)).value
),
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=WBGT_TEMP, key=WBGT_TEMP,
@ -488,6 +473,6 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=2, suggested_display_precision=2,
value_fn=lambda data: cast("int", data), value_fn=to_float,
), ),
) )

View File

@ -44,6 +44,7 @@
"windy": "Nastavení pro přeposílání dat na Windy", "windy": "Nastavení pro přeposílání dat na Windy",
"pocasi": "Nastavení pro přeposlání dat na Počasí Meteo CZ", "pocasi": "Nastavení pro přeposlání dat na Počasí Meteo CZ",
"ecowitt": "Nastavení pro stanice Ecowitt", "ecowitt": "Nastavení pro stanice Ecowitt",
"wslink_port_setup": "Nastavení portu WSLink Addonu",
"migration": "Migrace statistiky senzoru" "migration": "Migrace statistiky senzoru"
} }
}, },
@ -108,6 +109,16 @@
"ecowitt_enabled": "Povolit přijímání dat ze stanic Ecowitt" "ecowitt_enabled": "Povolit přijímání dat ze stanic Ecowitt"
} }
}, },
"wslink_port_setup": {
"description": "Nastavení portu, kde naslouchá WSLink Addon. Slouží pro příjem diagnostik.",
"title": "Port WSLink Addonu",
"data": {
"WSLINK_ADDON_PORT": "Naslouchající port WSLink Addonu"
},
"data_description": {
"WSLINK_ADDON_PORT": "Zadejte port, tak jak jej máte nastavený ve WSLink Addonu."
}
},
"migration": { "migration": {
"title": "Migrace statistiky senzoru.", "title": "Migrace statistiky senzoru.",
"description": "Pro správnou funkci dlouhodobé statistiky je nutné provést migraci jednotky senzoru v dlouhodobé statistice. Původní jednotka dlouhodobé statistiky pro denní úhrn srážek byla v mm/d, nicméně stanice zasílá pouze data v mm bez časového rozlišení.\n\n Senzor, který má být migrován je pro denní úhrn srážek. Pokud je v seznamu již správná hodnota u senzoru pro denní úhrn (mm), pak je již migrace hotová.\n\n Výsledek migrace pro senzor: {migration_status}, přepvedeno celkem {migration_count} řádků.", "description": "Pro správnou funkci dlouhodobé statistiky je nutné provést migraci jednotky senzoru v dlouhodobé statistice. Původní jednotka dlouhodobé statistiky pro denní úhrn srážek byla v mm/d, nicméně stanice zasílá pouze data v mm bez časového rozlišení.\n\n Senzor, který má být migrován je pro denní úhrn srážek. Pokud je v seznamu již správná hodnota u senzoru pro denní úhrn (mm), pak je již migrace hotová.\n\n Výsledek migrace pro senzor: {migration_status}, přepvedeno celkem {migration_count} řádků.",

View File

@ -204,8 +204,10 @@ def wind_dir_to_text(deg: float) -> UnitOfDir | None:
Returns UnitOfDir or None Returns UnitOfDir or None
""" """
if deg: _deg = to_float(deg)
return AZIMUT[int(abs((float(deg) - 11.25) % 360) / 22.5)] if _deg is not None:
_LOGGER.debug("wind_dir: %s", AZIMUT[int(abs((_deg - 11.25) % 360) / 22.5)])
return AZIMUT[int(abs((_deg - 11.25) % 360) / 22.5)]
return None return None
@ -263,11 +265,32 @@ def celsius_to_fahrenheit(celsius: float) -> float:
return celsius * 9.0 / 5.0 + 32 return celsius * 9.0 / 5.0 + 32
def _to_float(val: Any) -> float | None: def to_int(val: Any) -> int | None:
"""Convert int or string to int."""
if val is None:
return None
if isinstance(val, str) and val.strip() == "":
return None
try:
v = int(val)
except (TypeError, ValueError):
return None
else:
return v
def to_float(val: Any) -> float | None:
"""Convert int or string to float.""" """Convert int or string to float."""
if not val: if val is None:
return None return None
if isinstance(val, str) and val.strip() == "":
return None
try: try:
v = float(val) v = float(val)
except (TypeError, ValueError): except (TypeError, ValueError):
@ -284,14 +307,14 @@ def heat_index(
data: dict with temperature and humidity data: dict with temperature and humidity
convert: bool, convert recieved data from Celsius to Fahrenheit convert: bool, convert recieved data from Celsius to Fahrenheit
""" """
if (temp := _to_float(data.get(OUTSIDE_TEMP))) is None: if (temp := to_float(data.get(OUTSIDE_TEMP))) is None:
_LOGGER.error( _LOGGER.error(
"We are missing/invalid OUTSIDE TEMP (%s), cannot calculate wind chill index.", "We are missing/invalid OUTSIDE TEMP (%s), cannot calculate wind chill index.",
temp, temp,
) )
return None return None
if (rh := _to_float(data.get(OUTSIDE_HUMIDITY))) is None: if (rh := to_float(data.get(OUTSIDE_HUMIDITY))) is None:
_LOGGER.error( _LOGGER.error(
"We are missing/invalid OUTSIDE HUMIDITY (%s), cannot calculate wind chill index.", "We are missing/invalid OUTSIDE HUMIDITY (%s), cannot calculate wind chill index.",
rh, rh,
@ -335,8 +358,8 @@ def chill_index(
data: dict with temperature and wind speed data: dict with temperature and wind speed
convert: bool, convert recieved data from Celsius to Fahrenheit convert: bool, convert recieved data from Celsius to Fahrenheit
""" """
temp = _to_float(data.get(OUTSIDE_TEMP)) temp = to_float(data.get(OUTSIDE_TEMP))
wind = _to_float(data.get(WIND_SPEED)) wind = to_float(data.get(WIND_SPEED))
if temp is None: if temp is None:
_LOGGER.error( _LOGGER.error(