From 09b872d51f5f8c7ae427dd774ca58402b7de8591 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 24 Aug 2021 00:20:28 +0200 Subject: [PATCH] Add `sensor` platform for Tractive integration (#54143) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add sensor platform * Add extra_state_attributes * Add more constants * Add sensor.py to .coveragerc file * Use native value * Suggested change * Move SENSOR_TYPES to sensor platform * Add domain as prefix to the signal * Use TractiveEntity class * Add model.py to .coveragerc file * Clean up files * Add entity_class attribute to TractiveSensorEntityDescription class * TractiveEntity inherits from Entity * Suggested change * Define _attr_icon as class attribute Co-authored-by: Daniel Hjelseth Høyer --- .coveragerc | 2 + CODEOWNERS | 2 +- homeassistant/components/tractive/__init__.py | 36 +++- homeassistant/components/tractive/const.py | 4 + .../components/tractive/device_tracker.py | 16 +- homeassistant/components/tractive/entity.py | 22 +++ .../components/tractive/manifest.json | 3 +- homeassistant/components/tractive/sensor.py | 173 ++++++++++++++++++ 8 files changed, 236 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/tractive/entity.py create mode 100644 homeassistant/components/tractive/sensor.py diff --git a/.coveragerc b/.coveragerc index 85b36a2d7d4..810b4d92ce1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1096,6 +1096,8 @@ omit = homeassistant/components/trackr/device_tracker.py homeassistant/components/tractive/__init__.py homeassistant/components/tractive/device_tracker.py + homeassistant/components/tractive/entity.py + homeassistant/components/tractive/sensor.py homeassistant/components/tradfri/* homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 1a81b981dc5..d9652519ed8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -530,7 +530,7 @@ homeassistant/components/totalconnect/* @austinmroczek homeassistant/components/tplink/* @rytilahti @thegardenmonkey homeassistant/components/traccar/* @ludeeus homeassistant/components/trace/* @home-assistant/core -homeassistant/components/tractive/* @Danielhiversen @zhulik +homeassistant/components/tractive/* @Danielhiversen @zhulik @bieniu homeassistant/components/tradfri/* @janiversen homeassistant/components/trafikverket_train/* @endor-force homeassistant/components/trafikverket_weatherstation/* @endor-force diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index 78ee4c7ed97..60014852895 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -7,21 +7,29 @@ import logging import aiotractive from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + CONF_EMAIL, + CONF_PASSWORD, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( + ATTR_DAILY_GOAL, + ATTR_MINUTES_ACTIVE, DOMAIN, RECONNECT_INTERVAL, SERVER_UNAVAILABLE, + TRACKER_ACTIVITY_STATUS_UPDATED, TRACKER_HARDWARE_STATUS_UPDATED, TRACKER_POSITION_UPDATED, ) -PLATFORMS = ["device_tracker"] +PLATFORMS = ["device_tracker", "sensor"] _LOGGER = logging.getLogger(__name__) @@ -112,14 +120,15 @@ class TractiveClient: if server_was_unavailable: _LOGGER.debug("Tractive is back online") server_was_unavailable = False - if event["message"] != "tracker_status": - continue - if "hardware" in event: - self._send_hardware_update(event) + if event["message"] == "activity_update": + self._send_activity_update(event) + else: + if "hardware" in event: + self._send_hardware_update(event) - if "position" in event: - self._send_position_update(event) + if "position" in event: + self._send_position_update(event) except aiotractive.exceptions.TractiveError: _LOGGER.debug( "Tractive is not available. Internet connection is down? Sleeping %i seconds and retrying", @@ -133,11 +142,20 @@ class TractiveClient: continue def _send_hardware_update(self, event): - payload = {"battery_level": event["hardware"]["battery_level"]} + payload = {ATTR_BATTERY_LEVEL: event["hardware"]["battery_level"]} self._dispatch_tracker_event( TRACKER_HARDWARE_STATUS_UPDATED, event["tracker_id"], payload ) + def _send_activity_update(self, event): + payload = { + ATTR_MINUTES_ACTIVE: event["progress"]["achieved_minutes"], + ATTR_DAILY_GOAL: event["progress"]["goal_minutes"], + } + self._dispatch_tracker_event( + TRACKER_ACTIVITY_STATUS_UPDATED, event["pet_id"], payload + ) + def _send_position_update(self, event): payload = { "latitude": event["position"]["latlong"][0], diff --git a/homeassistant/components/tractive/const.py b/homeassistant/components/tractive/const.py index 7587fedfc4c..cb525d538e4 100644 --- a/homeassistant/components/tractive/const.py +++ b/homeassistant/components/tractive/const.py @@ -6,7 +6,11 @@ DOMAIN = "tractive" RECONNECT_INTERVAL = timedelta(seconds=10) +ATTR_DAILY_GOAL = "daily_goal" +ATTR_MINUTES_ACTIVE = "minutes_active" + TRACKER_HARDWARE_STATUS_UPDATED = f"{DOMAIN}_tracker_hardware_status_updated" TRACKER_POSITION_UPDATED = f"{DOMAIN}_tracker_position_updated" +TRACKER_ACTIVITY_STATUS_UPDATED = f"{DOMAIN}_tracker_activity_updated" SERVER_UNAVAILABLE = f"{DOMAIN}_server_unavailable" diff --git a/homeassistant/components/tractive/device_tracker.py b/homeassistant/components/tractive/device_tracker.py index 82e22139f04..c1652c27b8f 100644 --- a/homeassistant/components/tractive/device_tracker.py +++ b/homeassistant/components/tractive/device_tracker.py @@ -14,6 +14,7 @@ from .const import ( TRACKER_HARDWARE_STATUS_UPDATED, TRACKER_POSITION_UPDATED, ) +from .entity import TractiveEntity _LOGGER = logging.getLogger(__name__) @@ -45,29 +46,22 @@ async def create_trackable_entity(client, trackable): ) -class TractiveDeviceTracker(TrackerEntity): +class TractiveDeviceTracker(TractiveEntity, TrackerEntity): """Tractive device tracker.""" + _attr_icon = "mdi:paw" + def __init__(self, user_id, trackable, tracker_details, hw_info, pos_report): """Initialize tracker entity.""" - self._user_id = user_id + super().__init__(user_id, trackable, tracker_details) self._battery_level = hw_info["battery_level"] self._latitude = pos_report["latlong"][0] self._longitude = pos_report["latlong"][1] self._accuracy = pos_report["pos_uncertainty"] - self._tracker_id = tracker_details["_id"] self._attr_name = f"{self._tracker_id} {trackable['details']['name']}" self._attr_unique_id = trackable["_id"] - self._attr_icon = "mdi:paw" - self._attr_device_info = { - "identifiers": {(DOMAIN, self._tracker_id)}, - "name": f"Tractive ({self._tracker_id})", - "manufacturer": "Tractive GmbH", - "sw_version": tracker_details["fw_version"], - "model": tracker_details["model_number"], - } @property def source_type(self): diff --git a/homeassistant/components/tractive/entity.py b/homeassistant/components/tractive/entity.py new file mode 100644 index 00000000000..4ddc7f7aa35 --- /dev/null +++ b/homeassistant/components/tractive/entity.py @@ -0,0 +1,22 @@ +"""A entity class for Tractive integration.""" + +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN + + +class TractiveEntity(Entity): + """Tractive entity class.""" + + def __init__(self, user_id, trackable, tracker_details): + """Initialize tracker entity.""" + self._attr_device_info = { + "identifiers": {(DOMAIN, tracker_details["_id"])}, + "name": f"Tractive ({tracker_details['_id']})", + "manufacturer": "Tractive GmbH", + "sw_version": tracker_details["fw_version"], + "model": tracker_details["model_number"], + } + self._user_id = user_id + self._tracker_id = tracker_details["_id"] + self._trackable = trackable diff --git a/homeassistant/components/tractive/manifest.json b/homeassistant/components/tractive/manifest.json index 73ee75a4ac5..b388703e6bd 100644 --- a/homeassistant/components/tractive/manifest.json +++ b/homeassistant/components/tractive/manifest.json @@ -8,7 +8,8 @@ ], "codeowners": [ "@Danielhiversen", - "@zhulik" + "@zhulik", + "@bieniu" ], "iot_class": "cloud_push" } diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py new file mode 100644 index 00000000000..fdc38d8b83a --- /dev/null +++ b/homeassistant/components/tractive/sensor.py @@ -0,0 +1,173 @@ +"""Support for Tractive sensors.""" +from __future__ import annotations + +import asyncio +from dataclasses import dataclass + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + DEVICE_CLASS_BATTERY, + PERCENTAGE, + TIME_MINUTES, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import ( + ATTR_DAILY_GOAL, + ATTR_MINUTES_ACTIVE, + DOMAIN, + SERVER_UNAVAILABLE, + TRACKER_ACTIVITY_STATUS_UPDATED, + TRACKER_HARDWARE_STATUS_UPDATED, +) +from .entity import TractiveEntity + + +@dataclass +class TractiveSensorEntityDescription(SensorEntityDescription): + """Class describing Tractive sensor entities.""" + + attributes: tuple = () + entity_class: type[TractiveSensor] | None = None + + +class TractiveSensor(TractiveEntity, SensorEntity): + """Tractive sensor.""" + + def __init__(self, user_id, trackable, tracker_details, unique_id, description): + """Initialize sensor entity.""" + super().__init__(user_id, trackable, tracker_details) + + self._attr_unique_id = unique_id + self.entity_description = description + + @callback + def handle_server_unavailable(self): + """Handle server unavailable.""" + self._attr_available = False + self.async_write_ha_state() + + +class TractiveHardwareSensor(TractiveSensor): + """Tractive hardware sensor.""" + + def __init__(self, user_id, trackable, tracker_details, unique_id, description): + """Initialize sensor entity.""" + super().__init__(user_id, trackable, tracker_details, unique_id, description) + self._attr_name = f"{self._tracker_id} {description.name}" + + @callback + def handle_hardware_status_update(self, event): + """Handle hardware status update.""" + self._attr_native_value = event[self.entity_description.key] + self._attr_available = True + self.async_write_ha_state() + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{TRACKER_HARDWARE_STATUS_UPDATED}-{self._tracker_id}", + self.handle_hardware_status_update, + ) + ) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SERVER_UNAVAILABLE}-{self._user_id}", + self.handle_server_unavailable, + ) + ) + + +class TractiveActivitySensor(TractiveSensor): + """Tractive active sensor.""" + + def __init__(self, user_id, trackable, tracker_details, unique_id, description): + """Initialize sensor entity.""" + super().__init__(user_id, trackable, tracker_details, unique_id, description) + self._attr_name = f"{trackable['details']['name']} {description.name}" + + @callback + def handle_activity_status_update(self, event): + """Handle activity status update.""" + self._attr_native_value = event[self.entity_description.key] + self._attr_extra_state_attributes = { + attr: event[attr] if attr in event else None + for attr in self.entity_description.attributes + } + self._attr_available = True + self.async_write_ha_state() + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{TRACKER_ACTIVITY_STATUS_UPDATED}-{self._trackable['_id']}", + self.handle_activity_status_update, + ) + ) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SERVER_UNAVAILABLE}-{self._user_id}", + self.handle_server_unavailable, + ) + ) + + +SENSOR_TYPES = ( + TractiveSensorEntityDescription( + key=ATTR_BATTERY_LEVEL, + name="Battery Level", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + entity_class=TractiveHardwareSensor, + ), + TractiveSensorEntityDescription( + key=ATTR_MINUTES_ACTIVE, + name="Minutes Active", + icon="mdi:clock-time-eight-outline", + native_unit_of_measurement=TIME_MINUTES, + attributes=(ATTR_DAILY_GOAL,), + entity_class=TractiveActivitySensor, + ), +) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Tractive device trackers.""" + client = hass.data[DOMAIN][entry.entry_id] + + trackables = await client.trackable_objects() + + entities = [] + + async def _prepare_sensor_entity(item): + """Prepare sensor entities.""" + trackable = await item.details() + tracker = client.tracker(trackable["device_id"]) + tracker_details = await tracker.details() + for description in SENSOR_TYPES: + unique_id = f"{trackable['_id']}_{description.key}" + entities.append( + description.entity_class( + client.user_id, + trackable, + tracker_details, + unique_id, + description, + ) + ) + + await asyncio.gather(*(_prepare_sensor_entity(item) for item in trackables)) + + async_add_entities(entities)