diff --git a/README.md b/README.md index a9ac935..7af9e76 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ where `` is your Home Assistant configuration directory. ### Versions -This custom integration supports HomeAssistant versions 2021.12 or newer, using Python 3.9 or newer. +This custom integration supports HomeAssistant versions 2022.11 or newer. ### numpy on Raspberry Pi diff --git a/custom_components/composite/__init__.py b/custom_components/composite/__init__.py index 064190a..8dd8b83 100644 --- a/custom_components/composite/__init__.py +++ b/custom_components/composite/__init__.py @@ -6,22 +6,22 @@ import voluptuous as vol -from homeassistant.config import load_yaml_config_file -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.requirements import async_process_requirements, RequirementsNotFound from homeassistant.components.device_tracker import DOMAIN as DT_DOMAIN from homeassistant.components.device_tracker.legacy import YAML_DEVICES from homeassistant.components.persistent_notification import ( async_create as pn_async_create, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config import load_yaml_config_file +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_ID, CONF_NAME, CONF_PLATFORM, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType +from homeassistant.requirements import RequirementsNotFound, async_process_requirements from homeassistant.util import slugify +from .config_flow import split_conf from .const import ( CONF_DEFAULT_OPTIONS, CONF_REQ_MOVEMENT, @@ -36,7 +36,6 @@ TZ_DEVICE_LOCAL, TZ_DEVICE_UTC, ) -from .config_flow import split_conf from .device_tracker import COMPOSITE_TRACKER CONF_TZ_FINDER = "tz_finder" @@ -111,7 +110,7 @@ def _defaults(config: dict) -> dict: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Setup composite integration.""" + """Set up composite integration.""" hass.data[DOMAIN] = {DATA_LEGACY_WARNED: False} # Get a list of all the object IDs in known_devices.yaml to see if any were created @@ -125,8 +124,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: legacy_devices = {} try: legacy_ids = [ - cv.slugify(id) - for id, dev in legacy_devices.items() + cv.slugify(obj_id) + for obj_id, dev in legacy_devices.items() if cv.boolean(dev.get("track", False)) ] except vol.Invalid: @@ -144,13 +143,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: tracker_configs: list[dict[str, Any]] = config[DOMAIN][CONF_TRACKERS] conflict_ids: list[str] = [] for conf in tracker_configs: - id: str = conf[CONF_ID] + obj_id: str = conf[CONF_ID] - if id in legacy_ids: - conflict_ids.append(id) - elif id in cfg_entries: + if obj_id in legacy_ids: + conflict_ids.append(obj_id) + elif obj_id in cfg_entries: hass.config_entries.async_update_entry( - cfg_entries[id], **split_conf(conf) # type: ignore[arg-type] + cfg_entries[obj_id], **split_conf(conf) # type: ignore[arg-type] ) else: hass.async_create_task( @@ -192,8 +191,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: except RequirementsNotFound: _LOGGER.debug("Process requirements failed: %s", pkg) return False - else: - _LOGGER.debug("Process requirements suceeded: %s", pkg) + _LOGGER.debug("Process requirements suceeded: %s", pkg) def create_timefinder() -> None: """Create timefinder object.""" @@ -202,15 +200,21 @@ def create_timefinder() -> None: # does file I/O. if pkg.split("==")[0].strip().endswith("L"): - from timezonefinderL import TimezoneFinder + from timezonefinderL import ( # pylint: disable=import-outside-toplevel + TimezoneFinder, + ) tf = TimezoneFinder() elif config[DOMAIN][CONF_TZ_FINDER_CLASS] == "TimezoneFinder": - from timezonefinder import TimezoneFinder + from timezonefinder import ( # pylint: disable=import-outside-toplevel + TimezoneFinder, + ) tf = TimezoneFinder() else: - from timezonefinder import TimezoneFinderL + from timezonefinder import ( # pylint: disable=import-outside-toplevel + TimezoneFinderL, + ) tf = TimezoneFinderL() hass.data[DOMAIN][DATA_TF] = tf @@ -222,11 +226,7 @@ def create_timefinder() -> None: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up config entry.""" - # async_forward_entry_setups was new in 2022.8 - try: - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - except AttributeError: - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/custom_components/composite/config_flow.py b/custom_components/composite/config_flow.py index 08048b0..f00d103 100644 --- a/custom_components/composite/config_flow.py +++ b/custom_components/composite/config_flow.py @@ -4,8 +4,8 @@ from typing import Any from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult from homeassistant.const import CONF_ENTITY_ID, CONF_ID, CONF_NAME +from homeassistant.data_entry_flow import FlowResult from .const import CONF_REQ_MOVEMENT, CONF_TIME_AS, DOMAIN diff --git a/custom_components/composite/device_tracker.py b/custom_components/composite/device_tracker.py index 492b7f9..f638acf 100644 --- a/custom_components/composite/device_tracker.py +++ b/custom_components/composite/device_tracker.py @@ -22,35 +22,13 @@ ATTR_SOURCE_TYPE, DOMAIN as DT_DOMAIN, PLATFORM_SCHEMA as DT_PLATFORM_SCHEMA, + SourceType, ) - -# SourceType was new in 2022.9 -try: - from homeassistant.components.device_tracker import SourceType - - source_type_bluetooth: SourceType | str = SourceType.BLUETOOTH - source_type_bluetooth_le: SourceType | str = SourceType.BLUETOOTH_LE - source_type_gps: SourceType | str = SourceType.GPS - source_type_router: SourceType | str = SourceType.ROUTER -except ImportError: - from homeassistant.components.device_tracker import ( - SOURCE_TYPE_BLUETOOTH, - SOURCE_TYPE_BLUETOOTH_LE, - SOURCE_TYPE_GPS, - SOURCE_TYPE_ROUTER, - ) - - source_type_bluetooth = SOURCE_TYPE_BLUETOOTH - source_type_bluetooth_le = SOURCE_TYPE_BLUETOOTH_LE - source_type_gps = SOURCE_TYPE_GPS - source_type_router = SOURCE_TYPE_ROUTER - from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.components.persistent_notification import ( async_create as pn_async_create, ) -from homeassistant.components.zone import ENTITY_ID_HOME -from homeassistant.components.zone import async_active_zone +from homeassistant.components.zone import ENTITY_ID_HOME, async_active_zone from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_CHARGING, @@ -76,7 +54,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import track_state_change from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import GPSType, UNDEFINED, UndefinedType +from homeassistant.helpers.typing import UNDEFINED, GPSType, UndefinedType from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as dt_util from homeassistant.util.location import distance @@ -90,8 +68,8 @@ CONF_USE_PICTURE, DATA_LEGACY_WARNED, DATA_TF, - DEF_TIME_AS, DEF_REQ_MOVEMENT, + DEF_TIME_AS, DOMAIN, MIN_ANGLE_SPEED, MIN_SPEED_SECONDS, @@ -123,9 +101,9 @@ SOURCE_TYPE_NON_GPS = ( SOURCE_TYPE_BINARY_SENSOR, - source_type_bluetooth, - source_type_bluetooth_le, - source_type_router, + SourceType.BLUETOOTH, + SourceType.BLUETOOTH_LE, + SourceType.ROUTER, ) LAST_SEEN_ATTRS = (ATTR_LAST_SEEN, ATTR_LAST_TIMESTAMP) @@ -146,8 +124,7 @@ def _entities(entities: list[str | dict]) -> list[dict]: "composite tracker", path=[idx, CONF_USE_PICTURE], ) - else: - already_using_picture = True + already_using_picture = True result.append(entity) else: result.append( @@ -260,9 +237,9 @@ class CompositeDeviceTracker(TrackerEntity, RestoreEntity): def __init__(self, entry: ConfigEntry) -> None: """Initialize Composite Device Tracker.""" self._attr_name: str = entry.data[CONF_NAME] - id: str = entry.data[CONF_ID] - self._attr_unique_id = id - self.entity_id = f"{DT_DOMAIN}.{id}" + obj_id: str = entry.data[CONF_ID] + self._attr_unique_id = obj_id + self.entity_id = f"{DT_DOMAIN}.{obj_id}" self._scanner_config: dict | None = _config_from_entry(entry) self._lock = asyncio.Lock() @@ -348,13 +325,13 @@ async def _setup_scanner(self) -> None: if not self._scanner_config or self._scanner: return - def setup_scanner() -> None: + def setup_comp_scanner() -> None: """Set up device scanner.""" self._scanner = CompositeScanner( self.hass, cast(dict, self._scanner_config), self._see ) - await self.hass.async_add_executor_job(setup_scanner) + await self.hass.async_add_executor_job(setup_comp_scanner) async def _shutdown_scanner(self) -> None: """Shutdown device scanner.""" @@ -389,7 +366,7 @@ def _see( gps_accuracy: int | None = None, battery: int | None = None, attributes: dict | None = None, - source_type: str | None = source_type_gps, + source_type: SourceType | str | None = SourceType.GPS, picture: str | None | UndefinedType = UNDEFINED, ) -> None: """Process update from CompositeScanner.""" @@ -417,7 +394,7 @@ def _async_see( gps_accuracy: int | None = None, battery: int | None = None, attributes: dict | None = None, - source_type: str | None = source_type_gps, + source_type: SourceType | str | None = SourceType.GPS, picture: str | None | UndefinedType = UNDEFINED, ) -> None: """Process update from CompositeScanner.""" @@ -635,7 +612,7 @@ def _use_non_gps_data(self, entity_id: str, state: str) -> bool: if state == STATE_HOME or self._entities[entity_id].use_all_states: return True entities = self._entities.values() - if any(entity.source_type == source_type_gps for entity in entities): + if any(entity.source_type == SourceType.GPS for entity in entities): return False return all( cast(str, entity.data) != STATE_HOME @@ -651,7 +628,7 @@ def _dt_attr_from_utc(self, utc: datetime, tzone: tzinfo | None) -> datetime: return dt_util.as_local(utc) return utc - def _update_info( + def _update_info( # noqa: C901 self, entity_id: str, old_state: State | None, new_state: State | None ) -> None: """Update composite tracker from input entity state change.""" @@ -705,7 +682,7 @@ def _update_info( state = new_state.state - if source_type == source_type_gps: + if source_type == SourceType.GPS: # GPS coordinates and accuracy are required. if not gps: self._bad_entity(entity_id, "missing gps attributes") @@ -783,12 +760,12 @@ def _update_info( if state == STATE_HOME and cur_gps_is_home: gps = cast(GPSType, (cur_lat, cur_lon)) gps_accuracy = cur_acc - source_type = source_type_gps + source_type = SourceType.GPS # Otherwise, if new GPS data is valid (which is unlikely if # new state is not 'home'), # use it and make source_type gps. elif gps: - source_type = source_type_gps + source_type = SourceType.GPS # Otherwise, if new state is 'home' and old state is not 'home' # and no GPS data, then use HA's configured Home location and # make source_type gps. @@ -800,7 +777,7 @@ def _update_info( (self._hass.config.latitude, self._hass.config.longitude), ) gps_accuracy = 0 - source_type = source_type_gps + source_type = SourceType.GPS # Otherwise, don't use any GPS data, but set location_name to # new state. else: @@ -831,7 +808,7 @@ def _update_info( try: # timezone_at will return a string or None. tzname = self._tf.timezone_at(lng=gps[1], lat=gps[0]) - except Exception as exc: + except Exception as exc: # pylint: disable=broad-exception-caught _LOGGER.warning("Error while finding time zone: %s", exc) else: # get_time_zone will return a tzinfo or None. diff --git a/custom_components/composite/sensor.py b/custom_components/composite/sensor.py index 40ac061..f9b915f 100644 --- a/custom_components/composite/sensor.py +++ b/custom_components/composite/sensor.py @@ -11,41 +11,9 @@ 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 CONF_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, callback +from homeassistant.const import CONF_ID, CONF_NAME, UnitOfSpeed +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -56,7 +24,7 @@ class CompositeSensorEntityDescription(SensorEntityDescription): """Composite sensor entity description.""" - id: str = None # type: ignore[assignment] + obj_id: str = None # type: ignore[assignment] signal: str = None # type: ignore[assignment] @@ -67,16 +35,15 @@ async def async_setup_entry( ) -> None: """Set up the sensor platform.""" entity_description = CompositeSensorEntityDescription( - "speed", + key="speed", icon="mdi:car-speed-limiter", name=cast(str, entry.data[CONF_NAME]) + " Speed", + device_class=SensorDeviceClass.SPEED, + native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, - id=cast(str, entry.data[CONF_ID]) + "_speed", + obj_id=cast(str, entry.data[CONF_ID]) + "_speed", signal=f"{SIG_COMPOSITE_SPEED}-{entry.data[CONF_ID]}", ) - if speed_sensor_device_class: - entity_description.device_class = speed_sensor_device_class # type: ignore[assignment] - entity_description.native_unit_of_measurement = meters_per_second async_add_entities([CompositeSensor(hass, entity_description)]) @@ -84,7 +51,6 @@ class CompositeSensor(SensorEntity): """Composite Sensor Entity.""" _attr_should_poll = False - _to_unit: str | None = None _first_state_written = False def __init__( @@ -94,30 +60,12 @@ def __init__( assert entity_description.key == "speed" self.entity_description = entity_description - - @callback - def set_unit_of_measurement(event: Event | None = None) -> None: - """Set unit of measurement based on HA config.""" - if hass.config.units is METRIC_SYSTEM: - uom = SPEED_KILOMETERS_PER_HOUR - self._to_unit = LENGTH_KILOMETERS - else: - uom = SPEED_MILES_PER_HOUR - self._to_unit = LENGTH_MILES - self.entity_description.native_unit_of_measurement = uom - - if not entity_description.device_class: - set_unit_of_measurement() - self.async_on_remove( - hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, set_unit_of_measurement) - ) - - self._attr_unique_id = entity_description.id + self._attr_unique_id = entity_description.obj_id self._attr_extra_state_attributes = { ATTR_ANGLE: None, ATTR_DIRECTION: None, } - self.entity_id = f"{S_DOMAIN}.{entity_description.id}" + self.entity_id = f"{S_DOMAIN}.{entity_description.obj_id}" self.async_on_remove( async_dispatcher_connect(hass, entity_description.signal, self._update) @@ -140,10 +88,8 @@ def direction(angle: int | None) -> str | None: int((angle + 360 / 16) // (360 / 8)) ] - if value and self._to_unit: - value = f"{convert(value, LENGTH_METERS, self._to_unit) * (60 * 60):0.1f}" # type: ignore[assignment] self._attr_native_value = value - self.entity_description.force_update = bool(value) + self._attr_force_update = bool(value) self._attr_extra_state_attributes = { ATTR_ANGLE: angle, ATTR_DIRECTION: direction(angle), diff --git a/hacs.json b/hacs.json index afeed8b..e902306 100644 --- a/hacs.json +++ b/hacs.json @@ -1,4 +1,4 @@ { "name": "Composite Device Tracker", - "homeassistant": "2021.12.0b0" + "homeassistant": "2022.11" }