Improve type safety, add data validation, and refactor route handling
- Add typing and runtime checks with py_typecheck for config options and incoming data - Enhance authentication validation and error handling in data coordinator - Refactor Routes class with better dispatch logic and route enabling/disabling - Clean up async functions and improve logging consistency - Add type hints and annotations throughout the integration - Update manifest to include typecheck-runtime dependencyecowitt_support
parent
39cd852b36
commit
08b812e558
|
|
@ -1,14 +1,20 @@
|
||||||
"""The Sencor SWS 12500 Weather Station integration."""
|
"""The Sencor SWS 12500 Weather Station integration."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import aiohttp.web
|
import aiohttp.web
|
||||||
from aiohttp.web_exceptions import HTTPUnauthorized
|
from aiohttp.web_exceptions import HTTPUnauthorized
|
||||||
|
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 InvalidStateError, PlatformNotReady
|
from homeassistant.exceptions import (
|
||||||
|
ConfigEntryNotReady,
|
||||||
|
InvalidStateError,
|
||||||
|
PlatformNotReady,
|
||||||
|
)
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
|
@ -24,7 +30,7 @@ from .const import (
|
||||||
WSLINK_URL,
|
WSLINK_URL,
|
||||||
)
|
)
|
||||||
from .pocasti_cz import PocasiPush
|
from .pocasti_cz import PocasiPush
|
||||||
from .routes import Routes, unregistred
|
from .routes import Routes
|
||||||
from .utils import (
|
from .utils import (
|
||||||
anonymize,
|
anonymize,
|
||||||
check_disabled,
|
check_disabled,
|
||||||
|
|
@ -50,19 +56,20 @@ class WeatherDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, config: ConfigEntry) -> None:
|
def __init__(self, hass: HomeAssistant, config: ConfigEntry) -> None:
|
||||||
"""Init global updater."""
|
"""Init global updater."""
|
||||||
self.hass = hass
|
self.hass: HomeAssistant = hass
|
||||||
self.config = config
|
self.config: ConfigEntry = config
|
||||||
self.windy = WindyPush(hass, config)
|
self.windy: WindyPush = WindyPush(hass, config)
|
||||||
self.pocasi: PocasiPush = PocasiPush(hass, config)
|
self.pocasi: PocasiPush = PocasiPush(hass, config)
|
||||||
super().__init__(hass, _LOGGER, name=DOMAIN)
|
super().__init__(hass, _LOGGER, name=DOMAIN)
|
||||||
|
|
||||||
async def recieved_data(self, webdata):
|
async def recieved_data(self, webdata: aiohttp.web.Request) -> aiohttp.web.Response:
|
||||||
"""Handle incoming data query."""
|
"""Handle incoming data query."""
|
||||||
_wslink = self.config_entry.options.get(WSLINK)
|
|
||||||
data = webdata.query
|
|
||||||
|
|
||||||
response = None
|
_wslink: bool = checked_or(self.config.options.get(WSLINK), bool, False)
|
||||||
|
|
||||||
|
data: dict[str, Any] = dict(webdata.query)
|
||||||
|
|
||||||
|
# Check if station is sending auth data
|
||||||
if not _wslink and ("ID" not in data or "PASSWORD" not in data):
|
if not _wslink and ("ID" not in data or "PASSWORD" not in data):
|
||||||
_LOGGER.error("Invalid request. No security data provided!")
|
_LOGGER.error("Invalid request. No security data provided!")
|
||||||
raise HTTPUnauthorized
|
raise HTTPUnauthorized
|
||||||
|
|
@ -71,44 +78,67 @@ class WeatherDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
_LOGGER.error("Invalid request. No security data provided!")
|
_LOGGER.error("Invalid request. No security data provided!")
|
||||||
raise HTTPUnauthorized
|
raise HTTPUnauthorized
|
||||||
|
|
||||||
if _wslink:
|
id_data: str = ""
|
||||||
id_data = data["wsid"]
|
key_data: str = ""
|
||||||
key_data = data["wspw"]
|
|
||||||
else:
|
|
||||||
id_data = data["ID"]
|
|
||||||
key_data = data["PASSWORD"]
|
|
||||||
|
|
||||||
_id = self.config_entry.options.get(API_ID)
|
if _wslink:
|
||||||
_key = self.config_entry.options.get(API_KEY)
|
id_data = data.get("wsid", "")
|
||||||
|
key_data = data.get("wspw", "")
|
||||||
|
else:
|
||||||
|
id_data = data.get("ID", "")
|
||||||
|
key_data = data.get("PASSWORD", "")
|
||||||
|
|
||||||
|
# Check if we have valid auth data in the integration
|
||||||
|
|
||||||
|
if (_id := checked(self.config.options.get(API_ID), str)) is None:
|
||||||
|
_LOGGER.error("We don't have API ID set! Update your config!")
|
||||||
|
raise IncorrectDataError
|
||||||
|
|
||||||
|
if (_key := checked(self.config.options.get(API_KEY), str)) is None:
|
||||||
|
_LOGGER.error("We don't have API KEY set! Update your config!")
|
||||||
|
raise IncorrectDataError
|
||||||
|
|
||||||
if id_data != _id or key_data != _key:
|
if id_data != _id or key_data != _key:
|
||||||
_LOGGER.error("Unauthorised access!")
|
_LOGGER.error("Unauthorised access!")
|
||||||
raise HTTPUnauthorized
|
raise HTTPUnauthorized
|
||||||
|
|
||||||
if self.config_entry.options.get(WINDY_ENABLED):
|
if self.config.options.get(WINDY_ENABLED, False):
|
||||||
response = await self.windy.push_data_to_windy(data)
|
await self.windy.push_data_to_windy(data)
|
||||||
|
|
||||||
if self.config.options.get(POCASI_CZ_ENABLED):
|
if self.config.options.get(POCASI_CZ_ENABLED, False):
|
||||||
await self.pocasi.push_data_to_server(data, "WSLINK" if _wslink else "WU")
|
await self.pocasi.push_data_to_server(data, "WSLINK" if _wslink else "WU")
|
||||||
|
|
||||||
remaped_items = (
|
remaped_items: dict[str, str] = (
|
||||||
remap_wslink_items(data)
|
remap_wslink_items(data) if _wslink else remap_items(data)
|
||||||
if self.config_entry.options.get(WSLINK)
|
|
||||||
else remap_items(data)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if sensors := check_disabled(self.hass, remaped_items, self.config):
|
if sensors := check_disabled(remaped_items, self.config):
|
||||||
translate_sensors = [
|
if (
|
||||||
|
translate_sensors := checked(
|
||||||
|
[
|
||||||
await translations(
|
await translations(
|
||||||
self.hass, DOMAIN, f"sensor.{t_key}", key="name", category="entity"
|
self.hass,
|
||||||
|
DOMAIN,
|
||||||
|
f"sensor.{t_key}",
|
||||||
|
key="name",
|
||||||
|
category="entity",
|
||||||
)
|
)
|
||||||
for t_key in sensors
|
for t_key in sensors
|
||||||
if await translations(
|
if await translations(
|
||||||
self.hass, DOMAIN, f"sensor.{t_key}", key="name", category="entity"
|
self.hass,
|
||||||
|
DOMAIN,
|
||||||
|
f"sensor.{t_key}",
|
||||||
|
key="name",
|
||||||
|
category="entity",
|
||||||
)
|
)
|
||||||
is not None
|
is not None
|
||||||
]
|
],
|
||||||
human_readable = "\n".join(translate_sensors)
|
list[str],
|
||||||
|
)
|
||||||
|
) is not None:
|
||||||
|
human_readable: str = "\n".join(translate_sensors)
|
||||||
|
else:
|
||||||
|
human_readable = ""
|
||||||
|
|
||||||
await translated_notification(
|
await translated_notification(
|
||||||
self.hass,
|
self.hass,
|
||||||
|
|
@ -126,82 +156,42 @@ class WeatherDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
if self.config_entry.options.get(DEV_DBG):
|
if self.config_entry.options.get(DEV_DBG):
|
||||||
_LOGGER.info("Dev log: %s", anonymize(data))
|
_LOGGER.info("Dev log: %s", anonymize(data))
|
||||||
|
|
||||||
response = response or "OK"
|
return aiohttp.web.Response(body="OK", status=200)
|
||||||
return aiohttp.web.Response(body=f"{response or 'OK'}", status=200)
|
|
||||||
|
|
||||||
|
|
||||||
def register_path(
|
def register_path(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
url_path: str,
|
|
||||||
coordinator: WeatherDataUpdateCoordinator,
|
coordinator: WeatherDataUpdateCoordinator,
|
||||||
config: ConfigEntry,
|
config: ConfigEntry,
|
||||||
):
|
) -> bool:
|
||||||
"""Register path to handle incoming data."""
|
"""Register paths to webhook."""
|
||||||
|
|
||||||
hass_data = hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
debug = config.options.get(DEV_DBG)
|
if (hass_data := checked(hass.data[DOMAIN], dict[str, Any])) is None:
|
||||||
_wslink = config.options.get(WSLINK, False)
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
routes: Routes = hass_data.get("routes", Routes())
|
_wslink: bool = checked_or(config.options.get(WSLINK), bool, False)
|
||||||
|
|
||||||
if not routes.routes:
|
# Create internal route dispatcher with provided urls
|
||||||
routes = Routes()
|
routes: Routes = Routes()
|
||||||
_LOGGER.info("Routes not found, creating new routes")
|
routes.add_route(DEFAULT_URL, coordinator.recieved_data, enabled=not _wslink)
|
||||||
|
routes.add_route(WSLINK_URL, coordinator.recieved_data, enabled=_wslink)
|
||||||
if debug:
|
|
||||||
_LOGGER.debug("Enabled route is: %s, WSLink is %s", url_path, _wslink)
|
|
||||||
|
|
||||||
|
# Register webhooks in HomeAssistant with dispatcher
|
||||||
try:
|
try:
|
||||||
default_route = hass.http.app.router.add_get(
|
_ = hass.http.app.router.add_get(DEFAULT_URL, routes.dispatch)
|
||||||
DEFAULT_URL,
|
_ = hass.http.app.router.add_get(WSLINK_URL, routes.dispatch)
|
||||||
coordinator.recieved_data if not _wslink else unregistred,
|
|
||||||
name="weather_default_url",
|
|
||||||
)
|
|
||||||
if debug:
|
|
||||||
_LOGGER.debug("Default route: %s", default_route)
|
|
||||||
|
|
||||||
wslink_route = hass.http.app.router.add_get(
|
|
||||||
WSLINK_URL,
|
|
||||||
coordinator.recieved_data if _wslink else unregistred,
|
|
||||||
name="weather_wslink_url",
|
|
||||||
)
|
|
||||||
if debug:
|
|
||||||
_LOGGER.debug("WSLink route: %s", wslink_route)
|
|
||||||
|
|
||||||
routes.add_route(
|
|
||||||
DEFAULT_URL,
|
|
||||||
default_route,
|
|
||||||
coordinator.recieved_data if not _wslink else unregistred,
|
|
||||||
not _wslink,
|
|
||||||
)
|
|
||||||
routes.add_route(
|
|
||||||
WSLINK_URL, wslink_route, coordinator.recieved_data, _wslink
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# Save initialised routes
|
||||||
hass_data["routes"] = routes
|
hass_data["routes"] = routes
|
||||||
|
|
||||||
except RuntimeError as Ex: # pylint: disable=(broad-except)
|
except RuntimeError as Ex:
|
||||||
if (
|
_LOGGER.critical(
|
||||||
"Added route will never be executed, method GET is already registered"
|
"Routes cannot be added. Integration will not work as expected. %s", Ex
|
||||||
in Ex.args
|
|
||||||
):
|
|
||||||
_LOGGER.info("Handler to URL (%s) already registred", url_path)
|
|
||||||
return False
|
|
||||||
|
|
||||||
_LOGGER.error("Unable to register URL handler! (%s)", Ex.args)
|
|
||||||
return False
|
|
||||||
|
|
||||||
_LOGGER.info(
|
|
||||||
"Registered path to handle weather data: %s",
|
|
||||||
routes.get_enabled(), # pylint: disable=used-before-assignment
|
|
||||||
)
|
)
|
||||||
|
raise ConfigEntryNotReady from Ex
|
||||||
if _wslink:
|
|
||||||
routes.switch_route(coordinator.recieved_data, WSLINK_URL)
|
|
||||||
else:
|
else:
|
||||||
routes.switch_route(coordinator.recieved_data, DEFAULT_URL)
|
return True
|
||||||
|
|
||||||
return routes
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
|
@ -212,22 +202,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
hass_data = hass.data.setdefault(DOMAIN, {})
|
hass_data = hass.data.setdefault(DOMAIN, {})
|
||||||
hass_data[entry.entry_id] = coordinator
|
hass_data[entry.entry_id] = coordinator
|
||||||
|
|
||||||
_wslink = entry.options.get(WSLINK)
|
routes: Routes | None = hass_data.get("routes", None)
|
||||||
debug = entry.options.get(DEV_DBG)
|
|
||||||
|
_wslink = checked_or(entry.options.get(WSLINK), bool, False)
|
||||||
|
|
||||||
if debug:
|
|
||||||
_LOGGER.debug("WS Link is %s", "enbled" if _wslink else "disabled")
|
_LOGGER.debug("WS Link is %s", "enbled" if _wslink else "disabled")
|
||||||
|
|
||||||
route = register_path(
|
if routes:
|
||||||
hass, DEFAULT_URL if not _wslink else WSLINK_URL, coordinator, entry
|
_LOGGER.debug("We have routes registered, will try to switch dispatcher.")
|
||||||
)
|
routes.switch_route(DEFAULT_URL if not _wslink else WSLINK_URL)
|
||||||
|
_LOGGER.debug("%s", routes.show_enabled())
|
||||||
|
else:
|
||||||
|
routes_enabled = register_path(hass, coordinator, entry)
|
||||||
|
|
||||||
if not route:
|
if not routes_enabled:
|
||||||
_LOGGER.error("Fatal: path not registered!")
|
_LOGGER.error("Fatal: path not registered!")
|
||||||
raise PlatformNotReady
|
raise PlatformNotReady
|
||||||
|
|
||||||
hass_data["route"] = route
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||||
|
|
@ -238,7 +229,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
|
async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
"""Update setup listener."""
|
"""Update setup listener."""
|
||||||
|
|
||||||
await hass.config_entries.async_reload(entry.entry_id)
|
_ = await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
_LOGGER.info("Settings updated")
|
_LOGGER.info("Settings updated")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,12 @@ 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 ConfigFlow, ConfigFlowResult, OptionsFlow
|
from homeassistant.config_entries import (
|
||||||
|
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
|
||||||
|
|
@ -59,9 +64,9 @@ class ConfigOptionsFlowHandler(OptionsFlow):
|
||||||
self.ecowitt: dict[str, Any] = {}
|
self.ecowitt: dict[str, Any] = {}
|
||||||
self.ecowitt_schema = {}
|
self.ecowitt_schema = {}
|
||||||
|
|
||||||
@property
|
# @property
|
||||||
def config_entry(self):
|
# def config_entry(self) -> ConfigEntry:
|
||||||
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):
|
async def _get_entry_data(self):
|
||||||
"""Get entry data."""
|
"""Get entry data."""
|
||||||
|
|
@ -151,9 +156,9 @@ class ConfigOptionsFlowHandler(OptionsFlow):
|
||||||
step_id="init", menu_options=["basic", "ecowitt", "windy", "pocasi"]
|
step_id="init", menu_options=["basic", "ecowitt", "windy", "pocasi"]
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_basic(self, user_input=None):
|
async def async_step_basic(self, user_input: Any = None):
|
||||||
"""Manage basic options - credentials."""
|
"""Manage basic options - credentials."""
|
||||||
errors = {}
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
await self._get_entry_data()
|
await self._get_entry_data()
|
||||||
|
|
||||||
|
|
@ -184,9 +189,9 @@ class ConfigOptionsFlowHandler(OptionsFlow):
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_windy(self, user_input=None):
|
async def async_step_windy(self, user_input: Any = None):
|
||||||
"""Manage windy options."""
|
"""Manage windy options."""
|
||||||
errors = {}
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
await self._get_entry_data()
|
await self._get_entry_data()
|
||||||
|
|
||||||
|
|
@ -212,7 +217,7 @@ class ConfigOptionsFlowHandler(OptionsFlow):
|
||||||
async def async_step_pocasi(self, user_input: Any = None) -> ConfigFlowResult:
|
async def async_step_pocasi(self, user_input: Any = None) -> ConfigFlowResult:
|
||||||
"""Handle the pocasi step."""
|
"""Handle the pocasi step."""
|
||||||
|
|
||||||
errors = {}
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
await self._get_entry_data()
|
await self._get_entry_data()
|
||||||
|
|
||||||
|
|
@ -246,7 +251,7 @@ class ConfigOptionsFlowHandler(OptionsFlow):
|
||||||
async def async_step_ecowitt(self, user_input: Any = None) -> ConfigFlowResult:
|
async def async_step_ecowitt(self, user_input: Any = None) -> ConfigFlowResult:
|
||||||
"""Ecowitt stations setup."""
|
"""Ecowitt stations setup."""
|
||||||
|
|
||||||
errors = {}
|
errors: dict[str, str] = {}
|
||||||
await self._get_entry_data()
|
await self._get_entry_data()
|
||||||
|
|
||||||
if not (webhook := self.ecowitt.get(ECOWITT_WEBHOOK_ID)):
|
if not (webhook := self.ecowitt.get(ECOWITT_WEBHOOK_ID)):
|
||||||
|
|
@ -308,7 +313,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
async def async_step_user(self, user_input=None):
|
async def async_step_user(self, user_input: Any = None):
|
||||||
"""Handle the initial step."""
|
"""Handle the initial step."""
|
||||||
if user_input is None:
|
if user_input is None:
|
||||||
await self.async_set_unique_id(DOMAIN)
|
await self.async_set_unique_id(DOMAIN)
|
||||||
|
|
@ -319,7 +324,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
data_schema=vol.Schema(self.data_schema),
|
data_schema=vol.Schema(self.data_schema),
|
||||||
)
|
)
|
||||||
|
|
||||||
errors = {}
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
if user_input[API_ID] in INVALID_CREDENTIALS:
|
if user_input[API_ID] in INVALID_CREDENTIALS:
|
||||||
errors[API_ID] = "valid_credentials_api"
|
errors[API_ID] = "valid_credentials_api"
|
||||||
|
|
@ -340,6 +345,6 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(config_entry) -> ConfigOptionsFlowHandler:
|
def async_get_options_flow(config_entry: ConfigEntry) -> ConfigOptionsFlowHandler:
|
||||||
"""Get the options flow for this handler."""
|
"""Get the options flow for this handler."""
|
||||||
return ConfigOptionsFlowHandler()
|
return ConfigOptionsFlowHandler()
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
"homekit": {},
|
"homekit": {},
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"issue_tracker": "https://github.com/schizza/SWS-12500-custom-component/issues",
|
"issue_tracker": "https://github.com/schizza/SWS-12500-custom-component/issues",
|
||||||
"requirements": [],
|
"requirements": ["typecheck-runtime==0.2.0"],
|
||||||
"ssdp": [],
|
"ssdp": [],
|
||||||
"version": "1.6.9",
|
"version": "1.6.9",
|
||||||
"zeroconf": []
|
"zeroconf": []
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import logging
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
from aiohttp import ClientError
|
from aiohttp import ClientError
|
||||||
|
from py_typecheck.core import checked
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
@ -75,8 +76,20 @@ class PocasiPush:
|
||||||
"""Pushes weather data to server."""
|
"""Pushes weather data to server."""
|
||||||
|
|
||||||
_data = data.copy()
|
_data = data.copy()
|
||||||
_api_id = self.config.options.get(POCASI_CZ_API_ID)
|
|
||||||
_api_key = self.config.options.get(POCASI_CZ_API_KEY)
|
if (_api_id := checked(self.config.options.get(POCASI_CZ_API_ID), str)) is None:
|
||||||
|
_LOGGER.error(
|
||||||
|
"No API ID is provided for Pocasi Meteo. Check your configuration."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (
|
||||||
|
_api_key := checked(self.config.options.get(POCASI_CZ_API_KEY), str)
|
||||||
|
) is None:
|
||||||
|
_LOGGER.error(
|
||||||
|
"No API Key is provided for Pocasi Meteo. Check your configuration."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if self.log:
|
if self.log:
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
|
|
@ -91,7 +104,7 @@ class PocasiPush:
|
||||||
self._interval,
|
self._interval,
|
||||||
self.next_update,
|
self.next_update,
|
||||||
)
|
)
|
||||||
return False
|
return
|
||||||
|
|
||||||
request_url: str = ""
|
request_url: str = ""
|
||||||
if mode == "WSLINK":
|
if mode == "WSLINK":
|
||||||
|
|
@ -139,5 +152,3 @@ class PocasiPush:
|
||||||
|
|
||||||
if self.log:
|
if self.log:
|
||||||
_LOGGER.info("Next update: %s", str(self.next_update))
|
_LOGGER.info("Next update: %s", str(self.next_update))
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,67 @@
|
||||||
"""Store routes info."""
|
"""Routes implementation."""
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from logging import getLogger
|
import logging
|
||||||
|
|
||||||
from aiohttp.web import AbstractRoute, Response
|
from aiohttp.web import Request, Response
|
||||||
|
|
||||||
_LOGGER = getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
Handler = Callable[[Request], Awaitable[Response]]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Route:
|
class RouteInfo:
|
||||||
"""Store route info."""
|
"""Route struct."""
|
||||||
|
|
||||||
url_path: str
|
url_path: str
|
||||||
route: AbstractRoute
|
handler: Handler
|
||||||
handler: Callable
|
|
||||||
enabled: bool = False
|
enabled: bool = False
|
||||||
|
fallback: Handler = field(default_factory=lambda: unregistred)
|
||||||
def __str__(self):
|
|
||||||
"""Return string representation."""
|
|
||||||
return f"{self.url_path} -> {self.handler}"
|
|
||||||
|
|
||||||
|
|
||||||
class Routes:
|
class Routes:
|
||||||
"""Store routes info."""
|
"""Routes class."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize routes."""
|
"""Init."""
|
||||||
self.routes = {}
|
self.routes: dict[str, RouteInfo] = {}
|
||||||
|
|
||||||
def switch_route(self, coordinator: Callable, url_path: str):
|
async def dispatch(self, request: Request) -> Response:
|
||||||
"""Switch route."""
|
"""Dispatch."""
|
||||||
|
info = self.routes.get(request.path)
|
||||||
|
if not info:
|
||||||
|
_LOGGER.debug("Route %s is not registered!")
|
||||||
|
return await unregistred(request)
|
||||||
|
handler = info.handler if info.enabled else info.fallback
|
||||||
|
return await handler(request)
|
||||||
|
|
||||||
for url, route in self.routes.items():
|
def switch_route(self, url_path: str) -> None:
|
||||||
if url == url_path:
|
"""Switch route to new handler."""
|
||||||
_LOGGER.info("New coordinator to route: %s", route.url_path)
|
for path, info in self.routes.items():
|
||||||
route.enabled = True
|
info.enabled = path == url_path
|
||||||
route.handler = coordinator
|
|
||||||
route.route._handler = coordinator # noqa: SLF001
|
|
||||||
else:
|
|
||||||
route.enabled = False
|
|
||||||
route.handler = unregistred
|
|
||||||
route.route._handler = unregistred # noqa: SLF001
|
|
||||||
|
|
||||||
def add_route(
|
def add_route(
|
||||||
self,
|
self, url_path: str, handler: Handler, *, enabled: bool = False
|
||||||
url_path: str,
|
) -> None:
|
||||||
route: AbstractRoute,
|
"""Add route to dispatcher."""
|
||||||
handler: Callable,
|
|
||||||
enabled: bool = False,
|
|
||||||
):
|
|
||||||
"""Add route."""
|
|
||||||
self.routes[url_path] = Route(url_path, route, handler, enabled)
|
|
||||||
|
|
||||||
def get_route(self, url_path: str) -> Route:
|
self.routes[url_path] = RouteInfo(url_path, handler, enabled=enabled)
|
||||||
"""Get route."""
|
_LOGGER.debug("Registered dispatcher for route %s", url_path)
|
||||||
return self.routes.get(url_path, Route)
|
|
||||||
|
|
||||||
def get_enabled(self) -> str:
|
def show_enabled(self) -> str:
|
||||||
"""Get enabled routes."""
|
"""Show info of enabled route."""
|
||||||
enabled_routes = [
|
for url, route in self.routes.items():
|
||||||
route.url_path for route in self.routes.values() if route.enabled
|
if route.enabled:
|
||||||
]
|
return (
|
||||||
return "".join(enabled_routes) if enabled_routes else "None"
|
f"Dispatcher enabled for URL: {url}, with handler: {route.handler}"
|
||||||
|
)
|
||||||
def __str__(self):
|
return "No routes is enabled."
|
||||||
"""Return string representation."""
|
|
||||||
return "\n".join([str(route) for route in self.routes.values()])
|
|
||||||
|
|
||||||
|
|
||||||
async def unregistred(*args, **kwargs):
|
async def unregistred(request: Request) -> Response:
|
||||||
"""Unregister path to handle incoming data."""
|
"""Return unregistred error."""
|
||||||
|
_ = request
|
||||||
_LOGGER.error("Recieved data to unregistred webhook. Check your settings")
|
_LOGGER.debug("Received data to unregistred or disabled webhook.")
|
||||||
return Response(body=f"{'Unregistred webhook.'}", status=404)
|
return Response(text="Unregistred webhook. Check your settings.", status=400)
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
from pathlib import Path
|
from typing import cast
|
||||||
import sqlite3
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from py_typecheck import checked
|
||||||
|
from py_typecheck.core import checked_or
|
||||||
|
|
||||||
from homeassistant.components import persistent_notification
|
from homeassistant.components import persistent_notification
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
|
@ -15,7 +15,6 @@ from homeassistant.helpers.translation import async_get_translations
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
AZIMUT,
|
AZIMUT,
|
||||||
DATABASE_PATH,
|
|
||||||
DEV_DBG,
|
DEV_DBG,
|
||||||
OUTSIDE_HUMIDITY,
|
OUTSIDE_HUMIDITY,
|
||||||
OUTSIDE_TEMP,
|
OUTSIDE_TEMP,
|
||||||
|
|
@ -37,19 +36,19 @@ async def translations(
|
||||||
*,
|
*,
|
||||||
key: str = "message",
|
key: str = "message",
|
||||||
category: str = "notify",
|
category: str = "notify",
|
||||||
) -> str:
|
) -> str | None:
|
||||||
"""Get translated keys for domain."""
|
"""Get translated keys for domain."""
|
||||||
|
|
||||||
localize_key = f"component.{translation_domain}.{category}.{translation_key}.{key}"
|
localize_key = f"component.{translation_domain}.{category}.{translation_key}.{key}"
|
||||||
|
|
||||||
language = hass.config.language
|
language: str = hass.config.language
|
||||||
|
|
||||||
_translations = await async_get_translations(
|
_translations = await async_get_translations(
|
||||||
hass, language, category, [translation_domain]
|
hass, language, category, [translation_domain]
|
||||||
)
|
)
|
||||||
if localize_key in _translations:
|
if localize_key in _translations:
|
||||||
return _translations[localize_key]
|
return _translations[localize_key]
|
||||||
return ""
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def translated_notification(
|
async def translated_notification(
|
||||||
|
|
@ -70,7 +69,7 @@ async def translated_notification(
|
||||||
f"component.{translation_domain}.{category}.{translation_key}.title"
|
f"component.{translation_domain}.{category}.{translation_key}.title"
|
||||||
)
|
)
|
||||||
|
|
||||||
language = hass.config.language
|
language: str = cast("str", hass.config.language)
|
||||||
|
|
||||||
_translations = await async_get_translations(
|
_translations = await async_get_translations(
|
||||||
hass, language, category, [translation_domain]
|
hass, language, category, [translation_domain]
|
||||||
|
|
@ -91,7 +90,10 @@ async def translated_notification(
|
||||||
|
|
||||||
|
|
||||||
async def update_options(
|
async def update_options(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, update_key, update_value
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
update_key: str,
|
||||||
|
update_value: str | list[str] | bool,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Update config.options entry."""
|
"""Update config.options entry."""
|
||||||
conf = {**entry.options}
|
conf = {**entry.options}
|
||||||
|
|
@ -100,46 +102,43 @@ async def update_options(
|
||||||
return hass.config_entries.async_update_entry(entry, options=conf)
|
return hass.config_entries.async_update_entry(entry, options=conf)
|
||||||
|
|
||||||
|
|
||||||
def anonymize(data):
|
def anonymize(
|
||||||
|
data: dict[str, str | int | float | bool],
|
||||||
|
) -> dict[str, str | int | float | bool]:
|
||||||
"""Anoynimize recieved data."""
|
"""Anoynimize recieved data."""
|
||||||
|
anonym: dict[str, str] = {}
|
||||||
anonym = {}
|
return {
|
||||||
for k in data:
|
anonym[key]: value
|
||||||
if k not in {"ID", "PASSWORD", "wsid", "wspw"}:
|
for key, value in data.items()
|
||||||
anonym[k] = data[k]
|
if key not in {"ID", "PASSWORD", "wsid", "wspw"}
|
||||||
|
}
|
||||||
return anonym
|
|
||||||
|
|
||||||
|
|
||||||
def remap_items(entities):
|
def remap_items(entities: dict[str, str]) -> dict[str, str]:
|
||||||
"""Remap items in query."""
|
"""Remap items in query."""
|
||||||
items = {}
|
return {
|
||||||
for item in entities:
|
REMAP_ITEMS[key]: value for key, value in entities.items() if key in REMAP_ITEMS
|
||||||
if item in REMAP_ITEMS:
|
}
|
||||||
items[REMAP_ITEMS[item]] = entities[item]
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
def remap_wslink_items(entities):
|
def remap_wslink_items(entities: dict[str, str]) -> dict[str, str]:
|
||||||
"""Remap items in query for WSLink API."""
|
"""Remap items in query for WSLink API."""
|
||||||
items = {}
|
return {
|
||||||
for item in entities:
|
REMAP_WSLINK_ITEMS[key]: value
|
||||||
if item in REMAP_WSLINK_ITEMS:
|
for key, value in entities.items()
|
||||||
items[REMAP_WSLINK_ITEMS[item]] = entities[item]
|
if key in REMAP_WSLINK_ITEMS
|
||||||
|
}
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
def loaded_sensors(config_entry: ConfigEntry) -> list | None:
|
def loaded_sensors(config_entry: ConfigEntry) -> list[str]:
|
||||||
"""Get loaded sensors."""
|
"""Get loaded sensors."""
|
||||||
|
|
||||||
return config_entry.options.get(SENSORS_TO_LOAD) or []
|
return config_entry.options.get(SENSORS_TO_LOAD) or []
|
||||||
|
|
||||||
|
|
||||||
def check_disabled(
|
def check_disabled(
|
||||||
hass: HomeAssistant, items, config_entry: ConfigEntry
|
items: dict[str, str], config_entry: ConfigEntry
|
||||||
) -> list | None:
|
) -> list[str] | None:
|
||||||
"""Check if we have data for unloaded sensors.
|
"""Check if we have data for unloaded sensors.
|
||||||
|
|
||||||
If so, then add sensor to load queue.
|
If so, then add sensor to load queue.
|
||||||
|
|
@ -147,10 +146,11 @@ def check_disabled(
|
||||||
Returns list of found sensors or None
|
Returns list of found sensors or None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log: bool = config_entry.options.get(DEV_DBG, False)
|
log = checked_or(config_entry.options.get(DEV_DBG), bool, False)
|
||||||
|
|
||||||
entityFound: bool = False
|
entityFound: bool = False
|
||||||
_loaded_sensors = loaded_sensors(config_entry)
|
_loaded_sensors: list[str] = loaded_sensors(config_entry)
|
||||||
missing_sensors: list = []
|
missing_sensors: list[str] = []
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
if log:
|
if log:
|
||||||
|
|
@ -188,10 +188,10 @@ def battery_level_to_text(battery: int) -> UnitOfBat:
|
||||||
1: UnitOfBat.NORMAL,
|
1: UnitOfBat.NORMAL,
|
||||||
}
|
}
|
||||||
|
|
||||||
if battery is None:
|
if (v := checked(battery, int)) is None:
|
||||||
return UnitOfBat.UNKNOWN
|
return UnitOfBat.UNKNOWN
|
||||||
|
|
||||||
return level_map.get(int(battery), UnitOfBat.UNKNOWN)
|
return level_map.get(v, UnitOfBat.UNKNOWN)
|
||||||
|
|
||||||
|
|
||||||
def battery_level_to_icon(battery: UnitOfBat) -> str:
|
def battery_level_to_icon(battery: UnitOfBat) -> str:
|
||||||
|
|
@ -218,21 +218,21 @@ def celsius_to_fahrenheit(celsius: float) -> float:
|
||||||
return celsius * 9.0 / 5.0 + 32
|
return celsius * 9.0 / 5.0 + 32
|
||||||
|
|
||||||
|
|
||||||
def heat_index(data: Any, convert: bool = False) -> float | None:
|
def heat_index(
|
||||||
|
data: dict[str, int | float | str], convert: bool = False
|
||||||
|
) -> float | None:
|
||||||
"""Calculate heat index from temperature.
|
"""Calculate heat index from temperature.
|
||||||
|
|
||||||
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 := checked(data.get(OUTSIDE_TEMP), float)) is None:
|
||||||
temp = data.get(OUTSIDE_TEMP, None)
|
_LOGGER.error("We are missing OUTSIDE TEMP, cannot calculate heat index.")
|
||||||
rh = data.get(OUTSIDE_HUMIDITY, None)
|
|
||||||
|
|
||||||
if not temp or not rh:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
temp = float(temp)
|
if (rh := checked(data.get(OUTSIDE_HUMIDITY), float)) is None:
|
||||||
rh = float(rh)
|
_LOGGER.error("We are missing OUTSIDE HUMIDITY, cannot calculate heat index.")
|
||||||
|
return None
|
||||||
|
|
||||||
adjustment = None
|
adjustment = None
|
||||||
|
|
||||||
|
|
@ -263,21 +263,20 @@ def heat_index(data: Any, convert: bool = False) -> float | None:
|
||||||
return simple
|
return simple
|
||||||
|
|
||||||
|
|
||||||
def chill_index(data: Any, convert: bool = False) -> float | None:
|
def chill_index(
|
||||||
|
data: dict[str, str | float | int], convert: bool = False
|
||||||
|
) -> float | None:
|
||||||
"""Calculate wind chill index from temperature and wind speed.
|
"""Calculate wind chill index from temperature and wind speed.
|
||||||
|
|
||||||
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
|
||||||
"""
|
"""
|
||||||
|
if (temp := checked(data.get(OUTSIDE_TEMP), float)) is None:
|
||||||
temp = data.get(OUTSIDE_TEMP, None)
|
_LOGGER.error("We are missing OUTSIDE TEMP, cannot calculate wind chill index.")
|
||||||
wind = data.get(WIND_SPEED, None)
|
return None
|
||||||
|
if (wind := checked(data.get(WIND_SPEED), float)) is None:
|
||||||
if not temp or not wind:
|
_LOGGER.error("We are missing WIND SPEED, cannot calculate wind chill index.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
temp = float(temp)
|
|
||||||
wind = float(wind)
|
|
||||||
|
|
||||||
if convert:
|
if convert:
|
||||||
temp = celsius_to_fahrenheit(temp)
|
temp = celsius_to_fahrenheit(temp)
|
||||||
|
|
@ -294,109 +293,3 @@ def chill_index(data: Any, convert: bool = False) -> float | None:
|
||||||
if temp < 50 and wind > 3
|
if temp < 50 and wind > 3
|
||||||
else temp
|
else temp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def long_term_units_in_statistics_meta():
|
|
||||||
"""Get units in long term statitstics."""
|
|
||||||
sensor_units = []
|
|
||||||
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:
|
|
||||||
db.execute(
|
|
||||||
"""
|
|
||||||
SELECT statistic_id, unit_of_measurement from statistics_meta
|
|
||||||
WHERE statistic_id LIKE 'sensor.weather_station_sws%'
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
rows = db.fetchall()
|
|
||||||
sensor_units = {
|
|
||||||
statistic_id: f"{statistic_id} ({unit})" for statistic_id, unit in rows
|
|
||||||
}
|
|
||||||
|
|
||||||
except sqlite3.Error as e:
|
|
||||||
_LOGGER.error("Error during data migration: %s", e)
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
return sensor_units
|
|
||||||
|
|
||||||
|
|
||||||
async def migrate_data(hass: HomeAssistant, sensor_id: str | None = None) -> int | 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."""
|
|
||||||
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
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
from aiohttp.client_exceptions import ClientError
|
from aiohttp.client_exceptions import ClientError
|
||||||
|
from py_typecheck.core import checked
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
@ -52,22 +54,22 @@ class WindyPush:
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, config: ConfigEntry) -> None:
|
def __init__(self, hass: HomeAssistant, config: ConfigEntry) -> None:
|
||||||
"""Init."""
|
"""Init."""
|
||||||
self.hass = hass
|
self.hass: Final = hass
|
||||||
self.config = config
|
self.config: Final = config
|
||||||
|
|
||||||
""" lets wait for 1 minute to get initial data from station
|
""" lets wait for 1 minute to get initial data from station
|
||||||
and then try to push first data to Windy
|
and then try to push first data to Windy
|
||||||
"""
|
"""
|
||||||
self.last_update = datetime.now()
|
self.last_update: datetime = datetime.now()
|
||||||
self.next_update = datetime.now() + timed(minutes=1)
|
self.next_update: datetime = datetime.now() + timed(minutes=1)
|
||||||
|
|
||||||
self.log = self.config.options.get(WINDY_LOGGER_ENABLED)
|
self.log: bool = self.config.options.get(WINDY_LOGGER_ENABLED, False)
|
||||||
self.invalid_response_count = 0
|
self.invalid_response_count: int = 0
|
||||||
|
|
||||||
def verify_windy_response( # pylint: disable=useless-return
|
def verify_windy_response( # pylint: disable=useless-return
|
||||||
self,
|
self,
|
||||||
response: str,
|
response: str,
|
||||||
) -> WindyNotInserted | WindySuccess | WindyApiKeyError | None:
|
):
|
||||||
"""Verify answer form Windy."""
|
"""Verify answer form Windy."""
|
||||||
|
|
||||||
if self.log:
|
if self.log:
|
||||||
|
|
@ -85,9 +87,7 @@ class WindyPush:
|
||||||
if "Unauthorized" in response:
|
if "Unauthorized" in response:
|
||||||
raise WindyApiKeyError
|
raise WindyApiKeyError
|
||||||
|
|
||||||
return None
|
async def push_data_to_windy(self, data: dict[str, str]) -> bool:
|
||||||
|
|
||||||
async def push_data_to_windy(self, data):
|
|
||||||
"""Pushes weather data do Windy stations.
|
"""Pushes weather data do Windy stations.
|
||||||
|
|
||||||
Interval is 5 minutes, otherwise Windy would not accepts data.
|
Interval is 5 minutes, otherwise Windy would not accepts data.
|
||||||
|
|
@ -96,8 +96,6 @@ class WindyPush:
|
||||||
from station. But we need to do some clean up.
|
from station. But we need to do some clean up.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
text_for_test = None
|
|
||||||
|
|
||||||
if self.log:
|
if self.log:
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Windy last update = %s, next update at: %s",
|
"Windy last update = %s, next update at: %s",
|
||||||
|
|
@ -112,13 +110,18 @@ class WindyPush:
|
||||||
|
|
||||||
for purge in PURGE_DATA:
|
for purge in PURGE_DATA:
|
||||||
if purge in purged_data:
|
if purge in purged_data:
|
||||||
purged_data.pop(purge)
|
_ = purged_data.pop(purge)
|
||||||
|
|
||||||
if "dewptf" in purged_data:
|
if "dewptf" in purged_data:
|
||||||
dewpoint = round(((float(purged_data.pop("dewptf")) - 32) / 1.8), 1)
|
dewpoint = round(((float(purged_data.pop("dewptf")) - 32) / 1.8), 1)
|
||||||
purged_data["dewpoint"] = str(dewpoint)
|
purged_data["dewpoint"] = str(dewpoint)
|
||||||
|
|
||||||
windy_api_key = self.config.options.get(WINDY_API_KEY)
|
if (
|
||||||
|
windy_api_key := checked(self.config.options.get(WINDY_API_KEY), str)
|
||||||
|
) is None:
|
||||||
|
_LOGGER.error("Windy API key is not provided! Check your configuration.")
|
||||||
|
return False
|
||||||
|
|
||||||
request_url = f"{WINDY_URL}{windy_api_key}"
|
request_url = f"{WINDY_URL}{windy_api_key}"
|
||||||
|
|
||||||
if self.log:
|
if self.log:
|
||||||
|
|
@ -133,27 +136,33 @@ class WindyPush:
|
||||||
# log despite of settings
|
# log despite of settings
|
||||||
_LOGGER.error(WINDY_NOT_INSERTED)
|
_LOGGER.error(WINDY_NOT_INSERTED)
|
||||||
|
|
||||||
text_for_test = WINDY_NOT_INSERTED
|
|
||||||
|
|
||||||
except WindyApiKeyError:
|
except WindyApiKeyError:
|
||||||
# log despite of settings
|
# log despite of settings
|
||||||
_LOGGER.critical(WINDY_INVALID_KEY)
|
_LOGGER.critical(WINDY_INVALID_KEY)
|
||||||
text_for_test = WINDY_INVALID_KEY
|
|
||||||
|
|
||||||
await update_options(self.hass, self.config, WINDY_ENABLED, False)
|
if not (
|
||||||
|
await update_options(
|
||||||
|
self.hass, self.config, WINDY_ENABLED, False
|
||||||
|
)
|
||||||
|
):
|
||||||
|
_LOGGER.debug("Failed to set Windy option to false.")
|
||||||
|
|
||||||
except WindySuccess:
|
except WindySuccess:
|
||||||
if self.log:
|
if self.log:
|
||||||
_LOGGER.info(WINDY_SUCCESS)
|
_LOGGER.info(WINDY_SUCCESS)
|
||||||
text_for_test = WINDY_SUCCESS
|
else:
|
||||||
|
if self.log:
|
||||||
|
_LOGGER.debug(WINDY_NOT_INSERTED)
|
||||||
|
|
||||||
except ClientError as ex:
|
except ClientError as ex:
|
||||||
_LOGGER.critical("Invalid response from Windy: %s", str(ex))
|
_LOGGER.critical("Invalid response from Windy: %s", str(ex))
|
||||||
self.invalid_response_count += 1
|
self.invalid_response_count += 1
|
||||||
if self.invalid_response_count > 3:
|
if self.invalid_response_count > 3:
|
||||||
_LOGGER.critical(WINDY_UNEXPECTED)
|
_LOGGER.critical(WINDY_UNEXPECTED)
|
||||||
text_for_test = WINDY_UNEXPECTED
|
if not await update_options(
|
||||||
await update_options(self.hass, self.config, WINDY_ENABLED, False)
|
self.hass, self.config, WINDY_ENABLED, False
|
||||||
|
):
|
||||||
|
_LOGGER.debug("Failed to set Windy options to false.")
|
||||||
|
|
||||||
self.last_update = datetime.now()
|
self.last_update = datetime.now()
|
||||||
self.next_update = self.last_update + timed(minutes=5)
|
self.next_update = self.last_update + timed(minutes=5)
|
||||||
|
|
@ -161,6 +170,4 @@ class WindyPush:
|
||||||
if self.log:
|
if self.log:
|
||||||
_LOGGER.info("Next update: %s", str(self.next_update))
|
_LOGGER.info("Next update: %s", str(self.next_update))
|
||||||
|
|
||||||
if RESPONSE_FOR_TEST and text_for_test:
|
return True
|
||||||
return text_for_test
|
|
||||||
return None
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue