Skip to content

Commit

Permalink
Add (disabled) diff tz binary sensor & service type device info
Browse files Browse the repository at this point in the history
Move common code to helpers.py
  • Loading branch information
pnbruckner committed Dec 13, 2023
1 parent 7fad4a5 commit 917bc96
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 127 deletions.
75 changes: 5 additions & 70 deletions custom_components/entity_tz/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
46 changes: 46 additions & 0 deletions custom_components/entity_tz/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion custom_components/entity_tz/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
138 changes: 138 additions & 0 deletions custom_components/entity_tz/helpers.py
Original file line number Diff line number Diff line change
@@ -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,
)
)
Loading

0 comments on commit 917bc96

Please sign in to comment.