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.core import HomeAssistant
from homeassistant.exceptions import InvalidStateError, PlatformNotReady
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import API_ID, API_KEY, DEFAULT_URL, DEV_DBG, DOMAIN, WINDY_ENABLED
from .utils import anonymize, check_disabled, remap_items
from .const import (
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
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR]
PLATFORMS: list[Platform] = [Platform.SENSOR]
class IncorrectDataError(InvalidStateError):
@ -57,7 +72,23 @@ class WeatherDataUpdateCoordinator(DataUpdateCoordinator):
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)
@ -77,7 +108,10 @@ def register_path(
"GET", url_path, coordinator.recieved_data
)
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)
return True
@ -90,38 +124,22 @@ def register_path(
)
return True
def unregister_path(hass: HomeAssistant):
"""Unregister path to handle incoming data."""
_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:
"""Set up the config entry for my device."""
coordinator = WeatherDataUpdateCoordinator(hass, entry)
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
weather = Weather(hass, entry)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
if not register_path(hass, DEFAULT_URL, coordinator):
_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)
entry.async_on_unload(entry.add_update_listener(weather.setup_update_listener))
entry.async_on_unload(entry.add_update_listener(update_listener))
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:
"""Unload a config entry."""
@ -143,12 +171,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unregister_path(hass)
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,
DOMAIN,
INVALID_CREDENTIALS,
SENSORS_TO_LOAD,
WINDY_API_KEY,
WINDY_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,
}
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 = {
vol.Required(API_ID, default=self.user_data[API_ID] 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]:
errors["base"] = "valid_credentials_match"
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
user_input.update(self.windy_data)
#retain sensors
user_input.update(self.sensors)
return self.async_create_entry(title=DOMAIN, data=user_input)
self.user_data = user_input
@ -133,6 +133,9 @@ class ConfigOptionsFlowHandler(config_entries.OptionsFlow):
# retain user_data
user_input.update(self.user_data)
#retain senors
user_input.update(self.sensors)
return self.async_create_entry(title=DOMAIN, data=user_input)

View File

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

View File

@ -10,6 +10,6 @@
"iot_class": "local_push",
"requirements": [],
"ssdp": [],
"version": "0.1.2",
"version": "0.1.3",
"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.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.update_coordinator import CoordinatorEntity
@ -42,6 +42,7 @@ from .const import (
OUTSIDE_HUMIDITY,
OUTSIDE_TEMP,
RAIN,
SENSORS_TO_LOAD,
SOLAR_RADIATION,
UV,
WIND_DIR,
@ -177,6 +178,7 @@ SENSOR_TYPES: tuple[WeatherSensorEntityDescription, ...] = (
),
WeatherSensorEntityDescription(
key=UV,
name=UV,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UV_INDEX,
icon="mdi:sunglasses",
@ -191,7 +193,6 @@ SENSOR_TYPES: tuple[WeatherSensorEntityDescription, ...] = (
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
icon="mdi:weather-sunny",
translation_key=CH2_TEMP,
entity_registry_visible_default=False,
value_fn=lambda data: cast(float, data),
),
WeatherSensorEntityDescription(
@ -201,7 +202,6 @@ SENSOR_TYPES: tuple[WeatherSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.HUMIDITY,
icon="mdi:weather-sunny",
translation_key=CH2_HUMIDITY,
entity_registry_visible_default=False,
value_fn=lambda data: cast(int, data),
),
)
@ -215,9 +215,17 @@ async def async_setup_entry(
"""Set up Weather Station sensors."""
coordinator: WeatherDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
sensors = []
for description in SENSOR_TYPES:
sensors.append(WeatherSensor(hass, description, coordinator))
sensors_to_load: list = []
sensors: list = []
# 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)
@ -226,7 +234,6 @@ class WeatherSensor(
):
"""Implementation of Weather Sensor entity."""
entity_description: WeatherSensorEntityDescription
_attr_has_entity_name = True
_attr_should_poll = False
@ -245,25 +252,25 @@ class WeatherSensor(
self._data = 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()
self.coordinator.async_add_listener(self._handle_coordinator_update)
prev_state_data = await self.async_get_last_sensor_data()
prev_state = await self.async_get_last_state()
if not prev_state:
return
self._data = prev_state_data.native_value
if not self.entity_registry_visible_default:
self.entity_registry_visible_default = True
# prev_state_data = await self.async_get_last_sensor_data()
# prev_state = await self.async_get_last_state()
# if not prev_state:
# return
# self._data = prev_state_data.native_value
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._data = self.coordinator.data.get(self.entity_description.key)
super()._handle_coordinator_update()
self.async_write_ha_state()
@property
@ -272,9 +279,9 @@ class WeatherSensor(
return self.entity_description.value_fn(self._data)
@property
def state_class(self) -> str:
"""Return stateClass."""
return str(self.entity_description.state_class)
def suggested_entity_id(self) -> str:
"""Return name."""
return generate_entity_id("sensor.{}", self.entity_description.key)
@property
def device_info(self) -> DeviceInfo:

View File

@ -78,5 +78,11 @@
"ch2_temp": { "name": "Channel 2 temperature" },
"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_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_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
from homeassistant.components import persistent_notification
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
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__)
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
) -> None:
"""Update config.options entry."""
conf = {}
for k in entry.options:
conf[k] = entry.options[k]
conf = {**entry.options}
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):
@ -46,38 +97,34 @@ def remap_items(entities):
return items
def loaded_sensors(config_entry: ConfigEntry) -> list | None:
"""Get loaded sensors."""
async def check_disabled(hass: HomeAssistant, items, log: bool = False):
"""Check if we have data for disabed sensors.
return config_entry.options.get(SENSORS_TO_LOAD) if config_entry.options.get(SENSORS_TO_LOAD) else []
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)
eid: str = None
log: bool = config_entry.options.get(DEV_DBG)
entityFound: bool = False
_loaded_sensors = loaded_sensors(config_entry)
missing_sensors: list = []
for disabled in DISABLED_BY_DEFAULT:
for item in items:
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 log:
_LOGGER.info("Found sensor %s", eid)
if is_disabled:
if log:
_LOGGER.info("Sensor %s is hidden. Making visible", eid)
_ER.async_update_entity(eid, hidden_by=None)
if item not in _loaded_sensors:
missing_sensors.append(item)
entityFound = True
if log:
_LOGGER.info("Add sensor (%s) to loading queue", item)
elif not is_disabled and log:
_LOGGER.info("Sensor %s is visible.", eid)
return entityFound
return missing_sensors if entityFound else None