Skip to content

Commit

Permalink
Add (disabled) entity local time sensor
Browse files Browse the repository at this point in the history
  • Loading branch information
pnbruckner committed Dec 13, 2023
1 parent 4313298 commit 7fad4a5
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 47 deletions.
29 changes: 23 additions & 6 deletions custom_components/entity_tz/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Entity Time Zone Sensor."""
from __future__ import annotations

from typing import cast
from datetime import tzinfo
import re

from timezonefinder import TimezoneFinder

Expand All @@ -16,29 +17,35 @@
Platform,
)
from homeassistant.core import Event, HomeAssistant, State, callback, split_entity_id
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

PLATFORMS = [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_name(hass: HomeAssistant, state: State | None) -> str | None:
"""Get time zone name from latitude & longitude from state."""
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
return cast(str, hass.data[DOMAIN]["tzf"].timezone_at(lat=lat, lng=lng))
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:
Expand All @@ -62,7 +69,7 @@ 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_name(hass, state) != hass.config.time_zone:
if get_tz(hass, state) != dt_util.DEFAULT_TIME_ZONE:
zones.append(state.entity_id)
hass.data[DOMAIN]["zones"] = zones

Expand All @@ -79,6 +86,16 @@ def zones_filter(event: Event) -> bool:
async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool:
"""Set up composite integration."""
await init_hass_data(hass)

# From 1.0.0b2 or older: Convert unique_id from entry.entry_id -> entry.entry_id-time_zone
ent_reg = er.async_get(hass)
for entity in ent_reg.entities.values():
if entity.platform != DOMAIN:
continue
if _OLD_UNIQUE_ID.fullmatch(entity.unique_id):
new_unique_id = f"{entity.unique_id}-time_zone"
ent_reg.async_update_entity(entity.entity_id, new_unique_id=new_unique_id)

return True


Expand All @@ -98,7 +115,7 @@ def sensor_state_listener(event: Event) -> None:
or new_state.attributes.get(ATTR_LONGITUDE)
!= old_state.attributes.get(ATTR_LONGITUDE)
):
async_dispatcher_send(hass, signal(entry), new_state)
async_dispatcher_send(hass, signal(entry), get_tz(hass, new_state))

entry.async_on_unload(
async_track_state_change_event(
Expand Down
153 changes: 112 additions & 41 deletions custom_components/entity_tz/sensor.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
"""Entity Time Zone Sensor."""
from __future__ import annotations

import asyncio
from datetime import datetime, tzinfo
import logging
from typing import cast

from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorEntity
from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ENTITY_ID
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
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_name, signal
from . import get_tz, signal
from .const import ATTR_UTC_OFFSET

_LOGGER = logging.getLogger(__name__)
Expand All @@ -24,61 +32,124 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the sensor platform."""
async_add_entities([EntityTimeZoneSensor(entry)], True)
async_add_entities(
[EntityTimeZoneSensor(entry), EntityLocalTimeSensor(entry)], True
)


class EntityTimeZoneSensor(SensorEntity):
"""Entity Time Zone Sensor Entity."""
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):
"""Entity time zone sensor entity."""

_attr_icon = "mdi:map-clock"
_attr_should_poll = False

def __init__(self, entry: ConfigEntry) -> None:
"""Initialize entity time zone sensor entity."""
self._attr_name = f"{entry.title} Time zone"
self._attr_unique_id = entry.entry_id
self._entity_id = entry.data[CONF_ENTITY_ID]
self.entity_id = f"{SENSOR_DOMAIN}.{self._entity_id.split('.', 1)[1]}_time_zone"
self._dispatcher_connected = False
entity_description = SensorEntityDescription(
key="Time zone",
icon="mdi:map-clock",
)
super().__init__(entry, entity_description)

async def async_update(self) -> None:
"""Update sensor."""
self._update(self.hass.states.get(self._entity_id))
if not self._dispatcher_connected:

@callback
def entity_changed(new_state: State | None) -> None:
"""Handle entity change."""
self._update(new_state)
self.async_write_ha_state()

self.async_on_remove(
async_dispatcher_connect(
self.hass,
signal(cast(ConfigEntry, self.platform.config_entry)),
entity_changed,
)
)
self._dispatcher_connected = True

def _update(self, state: State | None) -> None:
"""Perform state update."""
_LOGGER.debug(
"%s: Updating from state of %s: %s", self.name, self._entity_id, state
)
self._attr_available = False

if (tz_name := get_tz_name(self.hass, state)) is None:
if self._tz is None:
return

self._attr_available = True
self._attr_native_value = tz_name
self._attr_native_value = str(self._tz)

self._attr_extra_state_attributes = {}
if not (tz := dt_util.get_time_zone(tz_name)):
return
if (offset := dt_util.now().astimezone(tz).utcoffset()) is None:
if (offset := dt_util.now().astimezone(self._tz).utcoffset()) is None:
return
self._attr_extra_state_attributes[ATTR_UTC_OFFSET] = (
offset.total_seconds() / 3600
)


class EntityLocalTimeSensor(ETZSensor):
"""Entity local time sensor entity."""

_attr_should_poll = False

def __init__(self, entry: ConfigEntry) -> None:
"""Initialize entity local time sensor entity."""
entity_description = SensorEntityDescription(
key="Local Time",
entity_registry_enabled_default=False,
icon="mdi:map-clock",
)
super().__init__(entry, entity_description)

@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)

@callback
def time_changed(_: datetime) -> None:
"""Handle entity change."""
self.async_schedule_update_ha_state(True)

self.async_on_remove(async_track_time_change(self.hass, time_changed, second=0))

async def async_update(self) -> None:
"""Update sensor."""
self._attr_available = False

if self._tz is None:
return

self._attr_available = True
state = dt_util.now(self._tz).time().isoformat("minutes")
if state[0] == "0":
state = state[1:]
self._attr_native_value = state

0 comments on commit 7fad4a5

Please sign in to comment.