Autoloading new sensors.

pull/12/head
schizza 2024-04-07 11:05:01 +02:00
parent 086c34042f
commit b62e6abccf
5 changed files with 104 additions and 97 deletions

View File

@ -8,15 +8,22 @@ 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, remap_items, 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 +64,11 @@ 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):
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 +88,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 +104,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,10 +127,16 @@ 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."""
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."""
@ -145,10 +149,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return _ok return _ok
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # async def async_setup(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the component. # """Set up the component.
This component can only be configured through the Integrations UI. # This component can only be configured through the Integrations UI.
""" # """
hass.data.setdefault(DOMAIN, {}) # hass.data.setdefault(DOMAIN, {})
return True
# await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# 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

@ -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,9 +215,17 @@ 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 = []
# 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) async_add_entities(sensors)
@ -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
@ -244,20 +251,18 @@ class WeatherSensor(
self._attr_unique_id = description.key self._attr_unique_id = description.key
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:
@ -272,9 +277,14 @@ 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
# def translation_key(self) -> str:
# """"Returns translation key."""
# return self.entity_description.translation_key
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:

View File

@ -3,16 +3,14 @@
import logging import logging
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 .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__)
def update_options( 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."""
@ -23,7 +21,7 @@ def update_options(
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):
@ -47,37 +45,26 @@ def remap_items(entities):
return items return items
async def check_disabled(hass: HomeAssistant, items, log: bool = False): def check_disabled(hass: HomeAssistant, items, config_entry: ConfigEntry) -> list | None:
"""Check if we have data for disabed sensors. """Check if we have data for unloaded sensors.
If so, then enable senosor. If so, then add sensor to load queue.
Returns True if sensor found else False 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: list = config_entry.options.get(SENSORS_TO_LOAD) if config_entry.options.get(SENSORS_TO_LOAD) else []
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: if item not in loaded_sensors:
eid = _ER.async_get_entity_id(Platform.SENSOR, DOMAIN, disabled) loaded_sensors.append(item)
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)
entityFound = True entityFound = True
if log:
_LOGGER.info("Add sensor (%s) to loading queue", item)
elif not is_disabled and log: return loaded_sensors if entityFound else None
_LOGGER.info("Sensor %s is visible.", eid)
return entityFound