Merge pull request #12 from schizza/7-ha-stops-registering-new-data

7 HA stops registering new data after a while.
pull/13/head v1.3.0
schizza 2024-04-17 13:15:16 +02:00 committed by GitHub
commit 5fdd594bae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 197 additions and 149 deletions

View File

@ -8,15 +8,30 @@ 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 InvalidStateError, PlatformNotReady
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import API_ID, API_KEY, DEFAULT_URL, DEV_DBG, DOMAIN, WINDY_ENABLED from .const import (
from .utils import anonymize, check_disabled, remap_items API_ID,
API_KEY,
DEFAULT_URL,
DEV_DBG,
DOMAIN,
SENSORS_TO_LOAD,
WINDY_ENABLED,
)
from .utils import (
anonymize,
check_disabled,
loaded_sensors,
remap_items,
translated_notification,
translations,
update_options,
)
from .windy_func import WindyPush from .windy_func import WindyPush
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR] PLATFORMS: list[Platform] = [Platform.SENSOR]
class IncorrectDataError(InvalidStateError): class IncorrectDataError(InvalidStateError):
@ -57,7 +72,23 @@ class WeatherDataUpdateCoordinator(DataUpdateCoordinator):
remaped_items = remap_items(data) remaped_items = remap_items(data)
await check_disabled(self.hass, remaped_items, self.config_entry.options.get(DEV_DBG)) if sensors := check_disabled(self.hass, remaped_items, self.config):
translate_sensors = [
await translations(self.hass, DOMAIN, f"sensor.{t_key}", key="name", category="entity")
for t_key in sensors
]
human_readable = "\n".join(translate_sensors)
await translated_notification(
self.hass,
DOMAIN,
"added",
{"added_sensors": f"{human_readable}\n"},
)
if _loaded_sensors := loaded_sensors(self.config_entry):
sensors.extend(_loaded_sensors)
await update_options(self.hass, self.config_entry, SENSORS_TO_LOAD, sensors)
await self.hass.config_entries.async_reload(self.config.entry_id)
self.async_set_updated_data(remaped_items) self.async_set_updated_data(remaped_items)
@ -77,7 +108,10 @@ def register_path(
"GET", url_path, coordinator.recieved_data "GET", url_path, coordinator.recieved_data
) )
except RuntimeError as Ex: # pylint: disable=(broad-except) except RuntimeError as Ex: # pylint: disable=(broad-except)
if "Added route will never be executed, method GET is already registered" in Ex.args: if (
"Added route will never be executed, method GET is already registered"
in Ex.args
):
_LOGGER.info("Handler to URL (%s) already registred", url_path) _LOGGER.info("Handler to URL (%s) already registred", url_path)
return True return True
@ -90,38 +124,22 @@ def register_path(
) )
return True return True
def unregister_path(hass: HomeAssistant): def unregister_path(hass: HomeAssistant):
"""Unregister path to handle incoming data.""" """Unregister path to handle incoming data."""
_LOGGER.error( _LOGGER.error(
"Unable to delete webhook from API! Restart HA before adding integration!" """Unable to delete webhook from API! Restart HA before adding integration!
If this error is raised while adding sensors or reloading configuration, you can ignore this error
"""
) )
class Weather(WeatherDataUpdateCoordinator):
"""Weather class."""
def __init__(self, hass: HomeAssistant, config) -> None:
"""Init class."""
self.hass = hass
super().__init__(hass, config)
async def setup_update_listener(self, hass: HomeAssistant, entry: ConfigEntry):
"""Update setup listener."""
await hass.config_entries.async_reload(entry.entry_id)
_LOGGER.info("Settings updated")
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the config entry for my device.""" """Set up the config entry for my device."""
coordinator = WeatherDataUpdateCoordinator(hass, entry) coordinator = WeatherDataUpdateCoordinator(hass, entry)
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
hass.data[DOMAIN][entry.entry_id] = coordinator
weather = Weather(hass, entry)
if not register_path(hass, DEFAULT_URL, coordinator): if not register_path(hass, DEFAULT_URL, coordinator):
_LOGGER.error("Fatal: path not registered!") _LOGGER.error("Fatal: path not registered!")
@ -129,11 +147,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
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(weather.setup_update_listener)) entry.async_on_unload(entry.add_update_listener(update_listener))
return True return True
async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
"""Update setup listener."""
# Disabled as on fire async_reload, integration stops writing data,
# and we don't need to reload config entry for proper work.
# await hass.config_entries.async_reload(entry.entry_id)
_LOGGER.info("Settings updated")
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
@ -143,12 +171,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unregister_path(hass) unregister_path(hass)
return _ok return _ok
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component.
This component can only be configured through the Integrations UI.
"""
hass.data.setdefault(DOMAIN, {})
return True

View File

@ -13,6 +13,7 @@ from .const import (
DEV_DBG, DEV_DBG,
DOMAIN, DOMAIN,
INVALID_CREDENTIALS, INVALID_CREDENTIALS,
SENSORS_TO_LOAD,
WINDY_API_KEY, WINDY_API_KEY,
WINDY_ENABLED, WINDY_ENABLED,
WINDY_LOGGER_ENABLED, WINDY_LOGGER_ENABLED,
@ -46,6 +47,10 @@ class ConfigOptionsFlowHandler(config_entries.OptionsFlow):
WINDY_LOGGER_ENABLED: self.config_entry.options.get(WINDY_LOGGER_ENABLED) if isinstance(self.config_entry.options.get(WINDY_LOGGER_ENABLED), bool) else False, WINDY_LOGGER_ENABLED: self.config_entry.options.get(WINDY_LOGGER_ENABLED) if isinstance(self.config_entry.options.get(WINDY_LOGGER_ENABLED), bool) else False,
} }
self.sensors: dict[str, Any] = {
SENSORS_TO_LOAD: self.config_entry.options.get(SENSORS_TO_LOAD) if isinstance(self.config_entry.options.get(SENSORS_TO_LOAD), list) else []
}
self.user_data_schema = { self.user_data_schema = {
vol.Required(API_ID, default=self.user_data[API_ID] or ""): str, vol.Required(API_ID, default=self.user_data[API_ID] or ""): str,
vol.Required(API_KEY, default=self.user_data[API_KEY] or ""): str, vol.Required(API_KEY, default=self.user_data[API_KEY] or ""): str,
@ -85,17 +90,12 @@ class ConfigOptionsFlowHandler(config_entries.OptionsFlow):
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:
# retain Windy options
data: dict = {}
data[WINDY_API_KEY] = self.config_entry.options.get(WINDY_API_KEY)
data[WINDY_ENABLED] = self.config_entry.options.get(WINDY_ENABLED)
data[WINDY_LOGGER_ENABLED] = self.config_entry.options.get(
WINDY_LOGGER_ENABLED
)
# retain windy data # retain windy data
user_input.update(self.windy_data) user_input.update(self.windy_data)
#retain sensors
user_input.update(self.sensors)
return self.async_create_entry(title=DOMAIN, data=user_input) return self.async_create_entry(title=DOMAIN, data=user_input)
self.user_data = user_input self.user_data = user_input
@ -133,6 +133,9 @@ class ConfigOptionsFlowHandler(config_entries.OptionsFlow):
# retain user_data # retain user_data
user_input.update(self.user_data) user_input.update(self.user_data)
#retain senors
user_input.update(self.sensors)
return self.async_create_entry(title=DOMAIN, data=user_input) return self.async_create_entry(title=DOMAIN, data=user_input)

View File

@ -11,6 +11,8 @@ ICON = "mdi:weather"
API_KEY = "API_KEY" API_KEY = "API_KEY"
API_ID = "API_ID" API_ID = "API_ID"
SENSORS_TO_LOAD: Final = "sensors_to_load"
DEV_DBG: Final = "dev_debug_checkbox" DEV_DBG: Final = "dev_debug_checkbox"
WINDY_API_KEY = "WINDY_API_KEY" WINDY_API_KEY = "WINDY_API_KEY"
@ -85,7 +87,5 @@ REMAP_ITEMS: dict = {
"soilmoisture": CH2_HUMIDITY, "soilmoisture": CH2_HUMIDITY,
} }
DISABLED_BY_DEFAULT: Final = [ DISABLED_BY_DEFAULT: Final = [CH2_TEMP, CH2_HUMIDITY]
CH2_TEMP,
CH2_HUMIDITY
]

View File

@ -10,6 +10,6 @@
"iot_class": "local_push", "iot_class": "local_push",
"requirements": [], "requirements": [],
"ssdp": [], "ssdp": [],
"version": "0.1.2", "version": "0.1.3",
"zeroconf": [] "zeroconf": []
} }

View File

@ -1,46 +0,0 @@
@property
def translation_key(self):
"""Return translation key."""
return self.entity_description.translation_key
@property
def device_class(self):
"""Return device class."""
return self.entity_description.device_class
@property
def name(self) -> str:
"""Return the name of the switch."""
return str(self.entity_description.name)
@property
def unique_id(self) -> str:
"""Return a unique, Home Assistant friendly identifier for this entity."""
return self.entity_description.key
@property
def native_value(self):
"""Return value of entity."""
return self._state
@property
def icon(self) -> str:
"""Return icon of entity."""
return str(self.entity_description.icon)
@property
def native_unit_of_measurement(self) -> str:
"""Return unit of measurement."""
return str(self.entity_description.native_unit_of_measurement)
@property
def state_class(self) -> str:
"""Return stateClass."""
return str(self.entity_description.state_class)
@property
def suggested_unit_of_measurement(self) -> str:
"""Return sugestet_unit_of_measurement."""
return str(self.entity_description.suggested_unit_of_measurement)

View File

@ -25,7 +25,7 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo, generate_entity_id
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -42,6 +42,7 @@ from .const import (
OUTSIDE_HUMIDITY, OUTSIDE_HUMIDITY,
OUTSIDE_TEMP, OUTSIDE_TEMP,
RAIN, RAIN,
SENSORS_TO_LOAD,
SOLAR_RADIATION, SOLAR_RADIATION,
UV, UV,
WIND_DIR, WIND_DIR,
@ -177,6 +178,7 @@ SENSOR_TYPES: tuple[WeatherSensorEntityDescription, ...] = (
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=UV, key=UV,
name=UV,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UV_INDEX, native_unit_of_measurement=UV_INDEX,
icon="mdi:sunglasses", icon="mdi:sunglasses",
@ -191,7 +193,6 @@ SENSOR_TYPES: 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,
entity_registry_visible_default=False,
value_fn=lambda data: cast(float, data), value_fn=lambda data: cast(float, data),
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
@ -201,7 +202,6 @@ SENSOR_TYPES: 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,
entity_registry_visible_default=False,
value_fn=lambda data: cast(int, data), value_fn=lambda data: cast(int, data),
), ),
) )
@ -215,10 +215,18 @@ async def async_setup_entry(
"""Set up Weather Station sensors.""" """Set up Weather Station sensors."""
coordinator: WeatherDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] coordinator: WeatherDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
sensors = []
for description in SENSOR_TYPES: sensors_to_load: list = []
sensors.append(WeatherSensor(hass, description, coordinator)) sensors: list = []
async_add_entities(sensors)
# Check if we have some sensors to load.
if sensors_to_load := config_entry.options.get(SENSORS_TO_LOAD):
sensors = [
WeatherSensor(hass, description, coordinator)
for description in SENSOR_TYPES
if description.key in sensors_to_load
]
async_add_entities(sensors)
class WeatherSensor( class WeatherSensor(
@ -226,7 +234,6 @@ class WeatherSensor(
): ):
"""Implementation of Weather Sensor entity.""" """Implementation of Weather Sensor entity."""
entity_description: WeatherSensorEntityDescription
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_should_poll = False _attr_should_poll = False
@ -245,25 +252,25 @@ class WeatherSensor(
self._data = None self._data = None
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Handle disabled entities that has previous data.""" """Handle listeners to reloaded sensors."""
await super().async_added_to_hass() await super().async_added_to_hass()
self.coordinator.async_add_listener(self._handle_coordinator_update) self.coordinator.async_add_listener(self._handle_coordinator_update)
prev_state_data = await self.async_get_last_sensor_data() # prev_state_data = await self.async_get_last_sensor_data()
prev_state = await self.async_get_last_state() # prev_state = await self.async_get_last_state()
if not prev_state: # if not prev_state:
return # return
self._data = prev_state_data.native_value # self._data = prev_state_data.native_value
if not self.entity_registry_visible_default:
self.entity_registry_visible_default = True
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator.""" """Handle updated data from the coordinator."""
self._data = self.coordinator.data.get(self.entity_description.key) self._data = self.coordinator.data.get(self.entity_description.key)
super()._handle_coordinator_update()
self.async_write_ha_state() self.async_write_ha_state()
@property @property
@ -272,9 +279,9 @@ class WeatherSensor(
return self.entity_description.value_fn(self._data) return self.entity_description.value_fn(self._data)
@property @property
def state_class(self) -> str: def suggested_entity_id(self) -> str:
"""Return stateClass.""" """Return name."""
return str(self.entity_description.state_class) return generate_entity_id("sensor.{}", self.entity_description.key)
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:

View File

@ -78,5 +78,11 @@
"ch2_temp": { "name": "Channel 2 temperature" }, "ch2_temp": { "name": "Channel 2 temperature" },
"ch2_humidity": { "name": "Channel 2 humidity" } "ch2_humidity": { "name": "Channel 2 humidity" }
} }
},
"notify": {
"added": {
"title": "New sensors for SWS 12500 found.",
"message": "{added_sensors}\n<b>HomeAssistant needs to be restarted for proper integreation run.</b>"
}
} }
} }

View File

@ -77,5 +77,11 @@
"ch2_temp": { "name": "Teplota senzoru 2" }, "ch2_temp": { "name": "Teplota senzoru 2" },
"ch2_humidity": { "name": "Vlhkost sensoru 2" } "ch2_humidity": { "name": "Vlhkost sensoru 2" }
} }
},
"notify": {
"added": {
"title": "Nalezeny nové senzory pro SWS 12500.",
"message": "{added_sensors}\n<b>Je třeba restartovat Home Assistenta pro správnou funkci komponenty.</b>"
}
} }
} }

View File

@ -78,5 +78,11 @@
"ch2_temp": { "name": "Channel 2 temperature" }, "ch2_temp": { "name": "Channel 2 temperature" },
"ch2_humidity": { "name": "Channel 2 humidity" } "ch2_humidity": { "name": "Channel 2 humidity" }
} }
},
"notify": {
"added": {
"title": "New sensors for SWS 12500 found.",
"message": "{added_sensors}\n<b>HomeAssistant needs to be restarted for proper integreation run.</b>"
}
} }
} }

View File

@ -2,28 +2,79 @@
import logging import logging
from homeassistant.components import persistent_notification
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers.translation import async_get_translations
from .const import DISABLED_BY_DEFAULT, DOMAIN, REMAP_ITEMS from .const import DEV_DBG, REMAP_ITEMS, SENSORS_TO_LOAD
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def translations(
hass: HomeAssistant,
translation_domain: str,
translation_key: str,
*,
key: str = "message",
category: str = "notify"
) -> str:
"""Get translated keys for domain."""
def update_options( localize_key = f"component.{translation_domain}.{category}.{translation_key}.{key}"
language = hass.config.language
_translations = await async_get_translations(
hass, language, category, [translation_domain]
)
if localize_key in _translations:
return _translations[localize_key]
async def translated_notification(
hass: HomeAssistant,
translation_domain: str,
translation_key: str,
translation_placeholders: dict[str, str] | None = None,
notification_id: str | None = None,
*,
key: str = "message",
category: str = "notify"
) -> str:
"""Translate notification."""
localize_key = f"component.{translation_domain}.{category}.{translation_key}.{key}"
localize_title = f"component.{translation_domain}.{category}.{translation_key}.title"
language = hass.config.language
_translations = await async_get_translations(
hass, language, category, [translation_domain]
)
if localize_key in _translations:
if not translation_placeholders:
persistent_notification.async_create(
hass,
_translations[localize_key],
_translations[localize_title],
notification_id,
)
else:
message = _translations[localize_key].format(**translation_placeholders)
persistent_notification.async_create(
hass, message, _translations[localize_title], notification_id
)
async def update_options(
hass: HomeAssistant, entry: ConfigEntry, update_key, update_value hass: HomeAssistant, entry: ConfigEntry, update_key, update_value
) -> None: ) -> None:
"""Update config.options entry.""" """Update config.options entry."""
conf = {} conf = {**entry.options}
for k in entry.options:
conf[k] = entry.options[k]
conf[update_key] = update_value conf[update_key] = update_value
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):
@ -46,38 +97,34 @@ def remap_items(entities):
return items return items
def loaded_sensors(config_entry: ConfigEntry) -> list | None:
"""Get loaded sensors."""
async def check_disabled(hass: HomeAssistant, items, log: bool = False): return config_entry.options.get(SENSORS_TO_LOAD) if config_entry.options.get(SENSORS_TO_LOAD) else []
"""Check if we have data for disabed sensors.
If so, then enable senosor. def check_disabled(
hass: HomeAssistant, items, config_entry: ConfigEntry
) -> list | None:
"""Check if we have data for unloaded sensors.
Returns True if sensor found else False If so, then add sensor to load queue.
Returns list of found sensors or None
""" """
_ER = er.async_get(hass) log: bool = config_entry.options.get(DEV_DBG)
eid: str = None
entityFound: bool = False entityFound: bool = False
_loaded_sensors = loaded_sensors(config_entry)
missing_sensors: list = []
for disabled in DISABLED_BY_DEFAULT: for item in items:
if log: if log:
_LOGGER.info("Checking %s", disabled) _LOGGER.info("Checking %s", item)
if disabled in items:
eid = _ER.async_get_entity_id(Platform.SENSOR, DOMAIN, disabled)
is_disabled = _ER.entities[eid].hidden
if item not in _loaded_sensors:
missing_sensors.append(item)
entityFound = True
if log: if log:
_LOGGER.info("Found sensor %s", eid) _LOGGER.info("Add sensor (%s) to loading queue", item)
if is_disabled: return missing_sensors if entityFound else None
if log:
_LOGGER.info("Sensor %s is hidden. Making visible", eid)
_ER.async_update_entity(eid, hidden_by=None)
entityFound = True
elif not is_disabled and log:
_LOGGER.info("Sensor %s is visible.", eid)
return entityFound