From 917bc966fdcbdbcce32ae6458beaa1081c8c28ab Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 13 Dec 2023 08:52:38 -0600 Subject: [PATCH] Add (disabled) diff tz binary sensor & service type device info Move common code to helpers.py --- custom_components/entity_tz/__init__.py | 75 +--------- custom_components/entity_tz/binary_sensor.py | 46 +++++++ custom_components/entity_tz/config_flow.py | 2 +- custom_components/entity_tz/helpers.py | 138 +++++++++++++++++++ custom_components/entity_tz/sensor.py | 62 +-------- 5 files changed, 196 insertions(+), 127 deletions(-) create mode 100644 custom_components/entity_tz/binary_sensor.py create mode 100644 custom_components/entity_tz/helpers.py diff --git a/custom_components/entity_tz/__init__.py b/custom_components/entity_tz/__init__.py index ad216c3..5dd4c13 100644 --- a/custom_components/entity_tz/__init__.py +++ b/custom_components/entity_tz/__init__.py @@ -1,88 +1,23 @@ """Entity Time Zone Sensor.""" from __future__ import annotations -from datetime import tzinfo import re -from timezonefinder import TimezoneFinder - -from homeassistant.components import zone from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_LATITUDE, - ATTR_LONGITUDE, - CONF_ENTITY_ID, - EVENT_CORE_CONFIG_UPDATE, - EVENT_STATE_CHANGED, - Platform, -) -from homeassistant.core import Event, HomeAssistant, State, callback, split_entity_id +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ENTITY_ID, Platform +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType -import homeassistant.util.dt as dt_util -from .const import DOMAIN, SIG_ENTITY_CHANGED +from .const import DOMAIN +from .helpers import get_tz, init_hass_data, signal -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] _OLD_UNIQUE_ID = re.compile(r"[0-9a-f]{32}") -def signal(entry: ConfigEntry) -> str: - """Return signal name derived from config entry.""" - return f"{SIG_ENTITY_CHANGED}-{entry.entry_id}" - - -def get_tz(hass: HomeAssistant, state: State | None) -> tzinfo | None: - """Get time zone from latitude & longitude from state.""" - if not state: - return None - lat = state.attributes.get(ATTR_LATITUDE) - lng = state.attributes.get(ATTR_LONGITUDE) - if lat is None or lng is None: - return None - tz_name = hass.data[DOMAIN]["tzf"].timezone_at(lat=lat, lng=lng) - if tz_name is None: - return None - return dt_util.get_time_zone(tz_name) - - -async def init_hass_data(hass: HomeAssistant) -> None: - """Initialize integration's data.""" - if DOMAIN in hass.data: - return - hass.data[DOMAIN] = {} - - def create_timefinder() -> None: - """Create timefinder object.""" - - # This must be done in an executor since the timefinder constructor - # does file I/O. - - hass.data[DOMAIN]["tzf"] = TimezoneFinder() - - await hass.async_add_executor_job(create_timefinder) - - @callback - def update_zones(_: Event | None = None) -> None: - """Update list of zones to use.""" - zones = [] - for state in hass.states.async_all(zone.DOMAIN): - if get_tz(hass, state) != dt_util.DEFAULT_TIME_ZONE: - zones.append(state.entity_id) - hass.data[DOMAIN]["zones"] = zones - - @callback - def zones_filter(event: Event) -> bool: - """Return if the state changed event is for a zone.""" - return split_entity_id(event.data["entity_id"])[0] == zone.DOMAIN - - update_zones() - hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, update_zones) - hass.bus.async_listen(EVENT_STATE_CHANGED, update_zones, zones_filter) - - async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool: """Set up composite integration.""" await init_hass_data(hass) diff --git a/custom_components/entity_tz/binary_sensor.py b/custom_components/entity_tz/binary_sensor.py new file mode 100644 index 0000000..3322e14 --- /dev/null +++ b/custom_components/entity_tz/binary_sensor.py @@ -0,0 +1,46 @@ +"""Entity Time Zone Binary Sensor.""" +from __future__ import annotations + +from homeassistant.components.binary_sensor import ( + DOMAIN as BS_DOMAIN, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import dt as dt_util + +from .helpers import ETZSensor + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the binary sensor platform.""" + async_add_entities([EntityDiffTZSensor(entry)], True) + + +class EntityDiffTZSensor(ETZSensor, BinarySensorEntity): + """Entity time zone sensor entity.""" + + def __init__(self, entry: ConfigEntry) -> None: + """Initialize entity time zone sensor entity.""" + entity_description = BinarySensorEntityDescription( + key="Diff TZ", + entity_registry_enabled_default=False, + icon="mdi:map-clock", + ) + super().__init__(entry, entity_description, BS_DOMAIN) + + async def async_update(self) -> None: + """Update sensor.""" + self._attr_available = False + + if self._tz is None: + return + + self._attr_available = True + self._attr_is_on = self._tz != dt_util.DEFAULT_TIME_ZONE diff --git a/custom_components/entity_tz/config_flow.py b/custom_components/entity_tz/config_flow.py index 269471c..06594fb 100644 --- a/custom_components/entity_tz/config_flow.py +++ b/custom_components/entity_tz/config_flow.py @@ -15,8 +15,8 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.selector import EntitySelector, EntitySelectorConfig -from . import init_hass_data from .const import DOMAIN +from .helpers import init_hass_data def _wrapped_entity_config_entry_title( diff --git a/custom_components/entity_tz/helpers.py b/custom_components/entity_tz/helpers.py new file mode 100644 index 0000000..32bba82 --- /dev/null +++ b/custom_components/entity_tz/helpers.py @@ -0,0 +1,138 @@ +"""Entity Time Zone Sensor Helpers.""" +from __future__ import annotations + +import asyncio +from datetime import tzinfo +from typing import cast + +from timezonefinder import TimezoneFinder + +from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_ENTITY_ID, + EVENT_CORE_CONFIG_UPDATE, + EVENT_STATE_CHANGED, +) +from homeassistant.core import Event, HomeAssistant, State, callback, split_entity_id +from homeassistant.helpers.device_registry import DeviceEntryType + +# Device Info moved to device_registry in 2023.9 +try: + from homeassistant.helpers.device_registry import DeviceInfo +except ImportError: + from homeassistant.helpers.entity import DeviceInfo # type: ignore[attr-defined] + +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity, EntityDescription +from homeassistant.helpers.entity_platform import EntityPlatform +from homeassistant.util import dt as dt_util, slugify + +from .const import DOMAIN, SIG_ENTITY_CHANGED + + +def signal(entry: ConfigEntry) -> str: + """Return signal name derived from config entry.""" + return f"{SIG_ENTITY_CHANGED}-{entry.entry_id}" + + +def get_tz(hass: HomeAssistant, state: State | None) -> tzinfo | None: + """Get time zone from latitude & longitude from state.""" + if not state: + return None + lat = state.attributes.get(ATTR_LATITUDE) + lng = state.attributes.get(ATTR_LONGITUDE) + if lat is None or lng is None: + return None + tz_name = hass.data[DOMAIN]["tzf"].timezone_at(lat=lat, lng=lng) + if tz_name is None: + return None + return dt_util.get_time_zone(tz_name) + + +async def init_hass_data(hass: HomeAssistant) -> None: + """Initialize integration's data.""" + if DOMAIN in hass.data: + return + hass.data[DOMAIN] = {} + + def create_timefinder() -> None: + """Create timefinder object.""" + + # This must be done in an executor since the timefinder constructor + # does file I/O. + + hass.data[DOMAIN]["tzf"] = TimezoneFinder() + + await hass.async_add_executor_job(create_timefinder) + + @callback + def update_zones(_: Event | None = None) -> None: + """Update list of zones to use.""" + zones = [] + for state in hass.states.async_all(ZONE_DOMAIN): + if get_tz(hass, state) != dt_util.DEFAULT_TIME_ZONE: + zones.append(state.entity_id) + hass.data[DOMAIN]["zones"] = zones + + @callback + def zones_filter(event: Event) -> bool: + """Return if the state changed event is for a zone.""" + return split_entity_id(event.data["entity_id"])[0] == ZONE_DOMAIN + + update_zones() + hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, update_zones) + hass.bus.async_listen(EVENT_STATE_CHANGED, update_zones, zones_filter) + + +class ETZSensor(Entity): + """Base entity.""" + + _attr_should_poll = False + _attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, DOMAIN)}, + name="Entity Time Zone", + ) + _tz: tzinfo | None + + def __init__( + self, + entry: ConfigEntry, + entity_description: EntityDescription, + domain: str, + ) -> None: + """Initialize sensor entity.""" + self.entity_description = entity_description + self.entity_description.name = f"{entry.title} {entity_description.key}" + slug = slugify(entity_description.key) + self._attr_unique_id = f"{entry.entry_id}-{slug}" + self._entity_id = entry.data[CONF_ENTITY_ID] + self.entity_id = f"{domain}.{self._entity_id.split('.', 1)[1]}_{slug}" + + @callback + def add_to_platform_start( + self, + hass: HomeAssistant, + platform: EntityPlatform, + parallel_updates: asyncio.Semaphore | None, + ) -> None: + """Start adding an entity to a platform.""" + super().add_to_platform_start(hass, platform, parallel_updates) + self._tz = get_tz(self.hass, self.hass.states.get(self._entity_id)) + + @callback + def entity_changed(tz: tzinfo | None) -> None: + """Handle entity change.""" + self._tz = tz + self.async_schedule_update_ha_state(True) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + signal(cast(ConfigEntry, self.platform.config_entry)), + entity_changed, + ) + ) diff --git a/custom_components/entity_tz/sensor.py b/custom_components/entity_tz/sensor.py index 8d5c8fb..88e6060 100644 --- a/custom_components/entity_tz/sensor.py +++ b/custom_components/entity_tz/sensor.py @@ -2,9 +2,8 @@ from __future__ import annotations import asyncio -from datetime import datetime, tzinfo +from datetime import datetime import logging -from typing import cast from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, @@ -12,16 +11,13 @@ SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ENTITY_ID from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback, EntityPlatform from homeassistant.helpers.event import async_track_time_change -from homeassistant.util import slugify import homeassistant.util.dt as dt_util -from . import get_tz, signal from .const import ATTR_UTC_OFFSET +from .helpers import ETZSensor _LOGGER = logging.getLogger(__name__) @@ -37,60 +33,16 @@ async def async_setup_entry( ) -class ETZSensor(SensorEntity): - """Base sensor entity.""" - - _tz: tzinfo | None - - def __init__( - self, entry: ConfigEntry, entity_description: SensorEntityDescription - ) -> None: - """Initialize sensor entity.""" - self.entity_description = entity_description - self.entity_description.name = f"{entry.title} {entity_description.key}" - slug = slugify(entity_description.key) - self._attr_unique_id = f"{entry.entry_id}-{slug}" - self._entity_id = entry.data[CONF_ENTITY_ID] - self.entity_id = f"{SENSOR_DOMAIN}.{self._entity_id.split('.', 1)[1]}_{slug}" - - @callback - def add_to_platform_start( - self, - hass: HomeAssistant, - platform: EntityPlatform, - parallel_updates: asyncio.Semaphore | None, - ) -> None: - """Start adding an entity to a platform.""" - super().add_to_platform_start(hass, platform, parallel_updates) - self._tz = get_tz(self.hass, self.hass.states.get(self._entity_id)) - - @callback - def entity_changed(tz: tzinfo | None) -> None: - """Handle entity change.""" - self._tz = tz - self.async_schedule_update_ha_state(True) - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - signal(cast(ConfigEntry, self.platform.config_entry)), - entity_changed, - ) - ) - - -class EntityTimeZoneSensor(ETZSensor): +class EntityTimeZoneSensor(ETZSensor, SensorEntity): """Entity time zone sensor entity.""" - _attr_should_poll = False - def __init__(self, entry: ConfigEntry) -> None: """Initialize entity time zone sensor entity.""" entity_description = SensorEntityDescription( key="Time zone", icon="mdi:map-clock", ) - super().__init__(entry, entity_description) + super().__init__(entry, entity_description, SENSOR_DOMAIN) async def async_update(self) -> None: """Update sensor.""" @@ -110,11 +62,9 @@ async def async_update(self) -> None: ) -class EntityLocalTimeSensor(ETZSensor): +class EntityLocalTimeSensor(ETZSensor, SensorEntity): """Entity local time sensor entity.""" - _attr_should_poll = False - def __init__(self, entry: ConfigEntry) -> None: """Initialize entity local time sensor entity.""" entity_description = SensorEntityDescription( @@ -122,7 +72,7 @@ def __init__(self, entry: ConfigEntry) -> None: entity_registry_enabled_default=False, icon="mdi:map-clock", ) - super().__init__(entry, entity_description) + super().__init__(entry, entity_description, SENSOR_DOMAIN) @callback def add_to_platform_start(