From b4c337eb146b25babd903c3bfbab8fa3eeec0015 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Mon, 11 Dec 2023 13:44:11 -0600 Subject: [PATCH] First commit --- .github/workflows/validate.yml | 26 ++++ .gitignore | 6 + LICENSE | 24 ++++ README.md | 32 +++++ custom_components/entity_tz/__init__.py | 59 ++++++++ custom_components/entity_tz/config_flow.py | 46 ++++++ custom_components/entity_tz/const.py | 4 + custom_components/entity_tz/manifest.json | 12 ++ custom_components/entity_tz/sensor.py | 133 ++++++++++++++++++ .../entity_tz/translations/en.json | 13 ++ hacs.json | 4 + info.md | 4 + 12 files changed, 363 insertions(+) create mode 100644 .github/workflows/validate.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 custom_components/entity_tz/__init__.py create mode 100644 custom_components/entity_tz/config_flow.py create mode 100644 custom_components/entity_tz/const.py create mode 100644 custom_components/entity_tz/manifest.json create mode 100644 custom_components/entity_tz/sensor.py create mode 100644 custom_components/entity_tz/translations/en.json create mode 100644 hacs.json create mode 100644 info.md diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..c3069e1 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,26 @@ +name: Validate + +on: + pull_request: + push: + +jobs: + validate-hassfest: + runs-on: ubuntu-latest + name: With hassfest + steps: + - name: 📥 Checkout the repository + uses: actions/checkout@v3 + + - name: 🏃 Hassfest validation + uses: "home-assistant/actions/hassfest@master" + + validate-hacs: + runs-on: ubuntu-latest + name: With HACS Action + steps: + - name: 🏃 HACS validation + uses: hacs/action@main + with: + category: integration + ignore: brands diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb2c767 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +.vscode/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cf1ab25 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/README.md b/README.md new file mode 100644 index 0000000..b8d499d --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# Entity Time Zone Sensor Entity Time Zone Sensor + +Creates a sensor that indicates the time zone in which another entity is located. +The entity must have `latitude` and `longitude` attributes. + +Follow the installation instructions below. + +## Installation +### With HACS +[![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://hacs.xyz/) + +You can use HACS to manage the installation and provide update notifications. + +1. Add this repo as a [custom repository](https://hacs.xyz/docs/faq/custom_repositories/): + +```text +https://github.com/pnbruckner/ha-entity-tz +``` + +2. Install the integration using the appropriate button on the HACS Integrations page. Search for "entity time zone". + +### Manual + +Place a copy of the files from [`custom_components/entity_tz`](custom_components/entity_tz) +in `/custom_components/entity_tz`, +where `` is your Home Assistant configuration directory. + +>__NOTE__: When downloading, make sure to use the `Raw` button from each file's page. + +### Versions + +This custom integration supports HomeAssistant versions 2023.4.0 or newer. diff --git a/custom_components/entity_tz/__init__.py b/custom_components/entity_tz/__init__.py new file mode 100644 index 0000000..de8c64d --- /dev/null +++ b/custom_components/entity_tz/__init__.py @@ -0,0 +1,59 @@ +"""Entity Time Zone Sensor.""" +from __future__ import annotations + +import asyncio +from collections.abc import Coroutine +from typing import Any + +from timezonefinder import TimezoneFinder +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ( + CONF_UNIQUE_ID, + EVENT_CORE_CONFIG_UPDATE, + SERVICE_RELOAD, + Platform, +) +from homeassistant.core import Event, HomeAssistant, ServiceCall +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import async_integration_yaml_config +from homeassistant.helpers.service import async_register_admin_service +from homeassistant.helpers.sun import get_astral_location +from homeassistant.helpers.typing import ConfigType + +from .const import DOMAIN + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up composite integration.""" + + 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] = TimezoneFinder() + + await hass.async_add_executor_job(create_timefinder) + return True + + +# async def entry_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: +# """Handle config entry update.""" +# await hass.config_entries.async_reload(entry.entry_id) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up config entry.""" + # entry.async_on_unload(entry.add_update_listener(entry_updated)) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/custom_components/entity_tz/config_flow.py b/custom_components/entity_tz/config_flow.py new file mode 100644 index 0000000..3cb9e8d --- /dev/null +++ b/custom_components/entity_tz/config_flow.py @@ -0,0 +1,46 @@ +"""Config flow for Illuminance integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ENTITY_ID +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.schema_config_entry_flow import wrapped_entity_config_entry_title +from homeassistant.helpers.selector import EntitySelector, EntitySelectorConfig + +from .const import DOMAIN + + +class EntityTimeZoneConfigFlow(ConfigFlow, domain=DOMAIN): + """Entity Time Zone config flow.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Start user config flow.""" + if user_input is not None: + title = wrapped_entity_config_entry_title(self.hass, user_input[CONF_ENTITY_ID]) + return self.async_create_entry(title=title, data=user_input) + + entity_ids = [ + state.entity_id + for state in self.hass.states.async_all() + if state.domain != "zone" + and all( + attr in state.attributes + for attr in (ATTR_LATITUDE, ATTR_LONGITUDE) + ) + ] + data_schema = vol.Schema( + { + vol.Required( + CONF_ENTITY_ID + ): EntitySelector(EntitySelectorConfig(include_entities=entity_ids)), + } + ) + return self.async_show_form(step_id="user", data_schema=data_schema) diff --git a/custom_components/entity_tz/const.py b/custom_components/entity_tz/const.py new file mode 100644 index 0000000..334f06b --- /dev/null +++ b/custom_components/entity_tz/const.py @@ -0,0 +1,4 @@ +"""Constants for Entity Time Zone integration.""" +DOMAIN = "entity_tz" + +ATTR_UTC_OFFSET = "utc_offset" diff --git a/custom_components/entity_tz/manifest.json b/custom_components/entity_tz/manifest.json new file mode 100644 index 0000000..43aa317 --- /dev/null +++ b/custom_components/entity_tz/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "entity_tz", + "name": "Entity Time Zone", + "codeowners": ["@pnbruckner"], + "config_flow": true, + "dependencies": [], + "documentation": "https://github.com/pnbruckner/ha-entity-tz/blob/master/README.md", + "iot_class": "calculated", + "issue_tracker": "https://github.com/pnbruckner/ha-entity-tz/issues", + "requirements": ["timezonefinder==5.2.0"], + "version": "1.0.0b0" +} diff --git a/custom_components/entity_tz/sensor.py b/custom_components/entity_tz/sensor.py new file mode 100644 index 0000000..c8cae04 --- /dev/null +++ b/custom_components/entity_tz/sensor.py @@ -0,0 +1,133 @@ +"""Entity Time Zone Sensor.""" +from __future__ import annotations + +from dataclasses import dataclass +import logging +from typing import cast + +from homeassistant.components.sensor import ( + DOMAIN as S_DOMAIN, + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) + +# SensorDeviceClass.SPEED was new in 2022.10 +speed_sensor_device_class: str | None +try: + from homeassistant.components.sensor import SensorDeviceClass + + speed_sensor_device_class = SensorDeviceClass.SPEED +except AttributeError: + speed_sensor_device_class = None + from homeassistant.const import ( + EVENT_CORE_CONFIG_UPDATE, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, + ) + from homeassistant.util.distance import convert + from homeassistant.util.unit_system import METRIC_SYSTEM + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ID, CONF_ENTITY_ID, CONF_NAME + +# UnitOfSpeed was new in 2022.11 +meters_per_second: str +try: + from homeassistant.const import UnitOfSpeed + + meters_per_second = UnitOfSpeed.METERS_PER_SECOND +except ImportError: + from homeassistant.const import SPEED_METERS_PER_SECOND + + meters_per_second = SPEED_METERS_PER_SECOND + +from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_state_change_event + +from .const import ATTR_UTC_OFFSET, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the sensor platform.""" + async_add_entities([EntityTimeZoneSensor(entry)], True) + + +class EntityTimeZoneSensor(SensorEntity): + """Entity Time Zone Sensor Entity.""" + + # _attr_has_entity_name = True + # _attr_extra_state_attributes = {} + _attr_icon = "mdi:map-clock" + _attr_should_poll = False + # _attr_native_value = "" + + def __init__( + self, entry: ConfigEntry + ) -> None: + """Initialize composite 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] + + async def async_update(self) -> None: + """Update sensor.""" + self._update(self.hass.states.get(self._entity_id)) + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass. + + To be extended by integrations. + """ + + @callback + def sensor_state_listener(event: Event) -> None: + """Process input entity state update.""" + new_state: State | None = event.data["new_state"] + old_state: State | None = event.data["old_state"] + if ( + not old_state + or not new_state + or new_state.attributes.get(ATTR_LATITUDE) != old_state.attributes.get(ATTR_LATITUDE) + or new_state.attributes.get(ATTR_LONGITUDE) != old_state.attributes.get(ATTR_LONGITUDE) + ): + self._update(new_state) + self.async_write_ha_state() + + self.async_on_remove(async_track_state_change_event(self.hass, self._entity_id, sensor_state_listener)) + + def _update(self, state: State | None) -> None: + """Perform state update.""" + _LOGGER.debug("Updating: %s", state) + self._attr_available = False + + if state is None: + return + latitude = state.attributes.get(ATTR_LATITUDE) + longitude = state.attributes.get(ATTR_LONGITUDE) + if latitude is None or longitude is None: + return + + _LOGGER.debug("Updating 2: lat=%s, lng=%s", latitude, longitude) + tz = self.hass.data[DOMAIN].timezone_at(lat=latitude, lng=longitude) + _LOGGER.debug("Updating 3: %s", tz) + if tz is None: + return + + self._attr_native_value = tz + # offset = 0 + # self._attr_extra_state_attributes = {ATTR_UTC_OFFSET: offset} + self._attr_available = True + _LOGGER.debug("Updating 4: %s, %s", self._attr_native_value, self._attr_available) diff --git a/custom_components/entity_tz/translations/en.json b/custom_components/entity_tz/translations/en.json new file mode 100644 index 0000000..b5472ee --- /dev/null +++ b/custom_components/entity_tz/translations/en.json @@ -0,0 +1,13 @@ +{ + "title": "Entity Time Zone", + "config": { + "step": { + "user": { + "title": "Select Entity with Location", + "data": { + "entity_id": "Entity" + } + } + } + } +} \ No newline at end of file diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..6def343 --- /dev/null +++ b/hacs.json @@ -0,0 +1,4 @@ +{ + "name": "Entity Time Zone", + "homeassistant": "2023.4.0" +} diff --git a/info.md b/info.md new file mode 100644 index 0000000..75b0d74 --- /dev/null +++ b/info.md @@ -0,0 +1,4 @@ +# Entity Time Zone Sensor Entity Time Zone Sensor + +Creates a sensor that indicates the time zone in which another entity is located. +The entity must have `latitude` and `longitude` attributes.