diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index 351b39f61e76a..e08ea954e2169 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -89,7 +89,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from error tractive = TractiveClient(hass, client, creds["user_id"], entry) - tractive.subscribe() try: trackable_objects = await client.trackable_objects() @@ -97,7 +96,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: *(_generate_trackables(client, item) for item in trackable_objects) ) except aiotractive.exceptions.TractiveError as error: - await tractive.unsubscribe() raise ConfigEntryNotReady from error # When the pet defined in Tractive has no tracker linked we get None as `trackable`. @@ -173,6 +171,14 @@ def user_id(self) -> str: """Return user id.""" return self._user_id + @property + def subscribed(self) -> bool: + """Return True if subscribed.""" + if self._listen_task is None: + return False + + return not self._listen_task.cancelled() + async def trackable_objects( self, ) -> list[aiotractive.trackable_object.TrackableObject]: diff --git a/homeassistant/components/tractive/binary_sensor.py b/homeassistant/components/tractive/binary_sensor.py index d7968f15bf858..940ff82687e9d 100644 --- a/homeassistant/components/tractive/binary_sensor.py +++ b/homeassistant/components/tractive/binary_sensor.py @@ -11,17 +11,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_BATTERY_CHARGING, EntityCategory from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import Trackables -from .const import ( - CLIENT, - DOMAIN, - SERVER_UNAVAILABLE, - TRACKABLES, - TRACKER_HARDWARE_STATUS_UPDATED, -) +from . import Trackables, TractiveClient +from .const import CLIENT, DOMAIN, TRACKABLES, TRACKER_HARDWARE_STATUS_UPDATED from .entity import TractiveEntity @@ -29,45 +22,29 @@ class TractiveBinarySensor(TractiveEntity, BinarySensorEntity): """Tractive sensor.""" def __init__( - self, user_id: str, item: Trackables, description: BinarySensorEntityDescription + self, + client: TractiveClient, + item: Trackables, + description: BinarySensorEntityDescription, ) -> None: """Initialize sensor entity.""" - super().__init__(user_id, item.trackable, item.tracker_details) + super().__init__( + client, + item.trackable, + item.tracker_details, + f"{TRACKER_HARDWARE_STATUS_UPDATED}-{item.tracker_details['_id']}", + ) self._attr_unique_id = f"{item.trackable['_id']}_{description.key}" - self.entity_description = description - - @callback - def handle_server_unavailable(self) -> None: - """Handle server unavailable.""" self._attr_available = False - self.async_write_ha_state() + self.entity_description = description @callback - def handle_hardware_status_update(self, event: dict[str, Any]) -> None: - """Handle hardware status update.""" + def handle_status_update(self, event: dict[str, Any]) -> None: + """Handle status update.""" self._attr_is_on = event[self.entity_description.key] - self._attr_available = True - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """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, - ) - ) + super().handle_status_update(event) SENSOR_TYPE = BinarySensorEntityDescription( @@ -86,7 +63,7 @@ async def async_setup_entry( trackables = hass.data[DOMAIN][entry.entry_id][TRACKABLES] entities = [ - TractiveBinarySensor(client.user_id, item, SENSOR_TYPE) + TractiveBinarySensor(client, item, SENSOR_TYPE) for item in trackables if item.tracker_details.get("charging_state") is not None ] diff --git a/homeassistant/components/tractive/device_tracker.py b/homeassistant/components/tractive/device_tracker.py index a97ea963362ca..0e373e1a44fab 100644 --- a/homeassistant/components/tractive/device_tracker.py +++ b/homeassistant/components/tractive/device_tracker.py @@ -9,7 +9,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import Trackables +from . import Trackables, TractiveClient from .const import ( CLIENT, DOMAIN, @@ -28,7 +28,7 @@ async def async_setup_entry( client = hass.data[DOMAIN][entry.entry_id][CLIENT] trackables = hass.data[DOMAIN][entry.entry_id][TRACKABLES] - entities = [TractiveDeviceTracker(client.user_id, item) for item in trackables] + entities = [TractiveDeviceTracker(client, item) for item in trackables] async_add_entities(entities) @@ -39,9 +39,14 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity): _attr_icon = "mdi:paw" _attr_translation_key = "tracker" - def __init__(self, user_id: str, item: Trackables) -> None: + def __init__(self, client: TractiveClient, item: Trackables) -> None: """Initialize tracker entity.""" - super().__init__(user_id, item.trackable, item.tracker_details) + super().__init__( + client, + item.trackable, + item.tracker_details, + f"{TRACKER_HARDWARE_STATUS_UPDATED}-{item.tracker_details['_id']}", + ) self._battery_level: int | None = item.hw_info.get("battery_level") self._latitude: float = item.pos_report["latlong"][0] @@ -94,18 +99,15 @@ def _handle_position_update(self, event: dict[str, Any]) -> None: self._attr_available = True self.async_write_ha_state() - @callback - def _handle_server_unavailable(self) -> None: - self._attr_available = False - self.async_write_ha_state() - async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" + if not self._client.subscribed: + self._client.subscribe() self.async_on_remove( async_dispatcher_connect( self.hass, - f"{TRACKER_HARDWARE_STATUS_UPDATED}-{self._tracker_id}", + self._dispatcher_signal, self._handle_hardware_status_update, ) ) @@ -122,6 +124,6 @@ async def async_added_to_hass(self) -> None: async_dispatcher_connect( self.hass, f"{SERVER_UNAVAILABLE}-{self._user_id}", - self._handle_server_unavailable, + self.handle_server_unavailable, ) ) diff --git a/homeassistant/components/tractive/entity.py b/homeassistant/components/tractive/entity.py index d142fe69db5bd..da7beb8bcdd31 100644 --- a/homeassistant/components/tractive/entity.py +++ b/homeassistant/components/tractive/entity.py @@ -3,10 +3,13 @@ from typing import Any +from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import DOMAIN +from . import TractiveClient +from .const import DOMAIN, SERVER_UNAVAILABLE class TractiveEntity(Entity): @@ -15,7 +18,11 @@ class TractiveEntity(Entity): _attr_has_entity_name = True def __init__( - self, user_id: str, trackable: dict[str, Any], tracker_details: dict[str, Any] + self, + client: TractiveClient, + trackable: dict[str, Any], + tracker_details: dict[str, Any], + dispatcher_signal: str, ) -> None: """Initialize tracker entity.""" self._attr_device_info = DeviceInfo( @@ -26,6 +33,40 @@ def __init__( sw_version=tracker_details["fw_version"], model=tracker_details["model_number"], ) - self._user_id = user_id + self._user_id = client.user_id self._tracker_id = tracker_details["_id"] - self._trackable = trackable + self._client = client + self._dispatcher_signal = dispatcher_signal + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + if not self._client.subscribed: + self._client.subscribe() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self._dispatcher_signal, + self.handle_status_update, + ) + ) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SERVER_UNAVAILABLE}-{self._user_id}", + self.handle_server_unavailable, + ) + ) + + @callback + def handle_status_update(self, event: dict[str, Any]) -> None: + """Handle status update.""" + self._attr_available = event[self.entity_description.key] is not None + self.async_write_ha_state() + + @callback + def handle_server_unavailable(self) -> None: + """Handle server unavailable.""" + self._attr_available = False + self.async_write_ha_state() diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index 493b627f9b400..6891b74d31b31 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -18,10 +18,9 @@ UnitOfTime, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import Trackables +from . import Trackables, TractiveClient from .const import ( ATTR_CALORIES, ATTR_DAILY_GOAL, @@ -32,7 +31,6 @@ ATTR_TRACKER_STATE, CLIENT, DOMAIN, - SERVER_UNAVAILABLE, TRACKABLES, TRACKER_ACTIVITY_STATUS_UPDATED, TRACKER_HARDWARE_STATUS_UPDATED, @@ -45,7 +43,7 @@ class TractiveRequiredKeysMixin: """Mixin for required keys.""" - entity_class: type[TractiveSensor] + signal_prefix: str @dataclass @@ -54,112 +52,39 @@ class TractiveSensorEntityDescription( ): """Class describing Tractive sensor entities.""" + hardware_sensor: bool = False + class TractiveSensor(TractiveEntity, SensorEntity): """Tractive sensor.""" def __init__( self, - user_id: str, + client: TractiveClient, item: Trackables, description: TractiveSensorEntityDescription, ) -> None: """Initialize sensor entity.""" - super().__init__(user_id, item.trackable, item.tracker_details) - - self._attr_unique_id = f"{item.trackable['_id']}_{description.key}" - self.entity_description = description - - @callback - def handle_server_unavailable(self) -> None: - """Handle server unavailable.""" - self._attr_available = False - self.async_write_ha_state() - - -class TractiveHardwareSensor(TractiveSensor): - """Tractive hardware sensor.""" - - @callback - def handle_hardware_status_update(self, event: dict[str, Any]) -> None: - """Handle hardware status update.""" - if (_state := event[self.entity_description.key]) is None: - return - self._attr_native_value = _state - self._attr_available = True - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """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, + if description.hardware_sensor: + dispatcher_signal = ( + f"{description.signal_prefix}-{item.tracker_details['_id']}" ) + else: + dispatcher_signal = f"{description.signal_prefix}-{item.trackable['_id']}" + super().__init__( + client, item.trackable, item.tracker_details, dispatcher_signal ) - -class TractiveActivitySensor(TractiveSensor): - """Tractive active sensor.""" + self._attr_unique_id = f"{item.trackable['_id']}_{description.key}" + self._attr_available = False + self.entity_description = description @callback def handle_status_update(self, event: dict[str, Any]) -> None: """Handle 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) -> None: - """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_status_update, - ) - ) - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{SERVER_UNAVAILABLE}-{self._user_id}", - self.handle_server_unavailable, - ) - ) - -class TractiveWellnessSensor(TractiveActivitySensor): - """Tractive wellness sensor.""" - - async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{TRACKER_WELLNESS_STATUS_UPDATED}-{self._trackable['_id']}", - self.handle_status_update, - ) - ) - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{SERVER_UNAVAILABLE}-{self._user_id}", - self.handle_server_unavailable, - ) - ) + super().handle_status_update(event) SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( @@ -168,13 +93,15 @@ async def async_added_to_hass(self) -> None: translation_key="tracker_battery_level", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, - entity_class=TractiveHardwareSensor, + signal_prefix=TRACKER_HARDWARE_STATUS_UPDATED, + hardware_sensor=True, entity_category=EntityCategory.DIAGNOSTIC, ), TractiveSensorEntityDescription( key=ATTR_TRACKER_STATE, translation_key="tracker_state", - entity_class=TractiveHardwareSensor, + signal_prefix=TRACKER_HARDWARE_STATUS_UPDATED, + hardware_sensor=True, icon="mdi:radar", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.ENUM, @@ -190,7 +117,7 @@ async def async_added_to_hass(self) -> None: translation_key="activity_time", icon="mdi:clock-time-eight-outline", native_unit_of_measurement=UnitOfTime.MINUTES, - entity_class=TractiveActivitySensor, + signal_prefix=TRACKER_ACTIVITY_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, ), TractiveSensorEntityDescription( @@ -198,7 +125,7 @@ async def async_added_to_hass(self) -> None: translation_key="rest_time", icon="mdi:clock-time-eight-outline", native_unit_of_measurement=UnitOfTime.MINUTES, - entity_class=TractiveWellnessSensor, + signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, ), TractiveSensorEntityDescription( @@ -206,7 +133,7 @@ async def async_added_to_hass(self) -> None: translation_key="calories", icon="mdi:fire", native_unit_of_measurement="kcal", - entity_class=TractiveWellnessSensor, + signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, ), TractiveSensorEntityDescription( @@ -214,14 +141,14 @@ async def async_added_to_hass(self) -> None: translation_key="daily_goal", icon="mdi:flag-checkered", native_unit_of_measurement=UnitOfTime.MINUTES, - entity_class=TractiveActivitySensor, + signal_prefix=TRACKER_ACTIVITY_STATUS_UPDATED, ), TractiveSensorEntityDescription( key=ATTR_MINUTES_DAY_SLEEP, translation_key="minutes_day_sleep", icon="mdi:sleep", native_unit_of_measurement=UnitOfTime.MINUTES, - entity_class=TractiveWellnessSensor, + signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, ), TractiveSensorEntityDescription( @@ -229,7 +156,7 @@ async def async_added_to_hass(self) -> None: translation_key="minutes_night_sleep", icon="mdi:sleep", native_unit_of_measurement=UnitOfTime.MINUTES, - entity_class=TractiveWellnessSensor, + signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, ), ) @@ -243,7 +170,7 @@ async def async_setup_entry( trackables = hass.data[DOMAIN][entry.entry_id][TRACKABLES] entities = [ - description.entity_class(client.user_id, item, description) + TractiveSensor(client, item, description) for description in SENSOR_TYPES for item in trackables ] diff --git a/homeassistant/components/tractive/switch.py b/homeassistant/components/tractive/switch.py index 6d8274df2538e..55acdb9bdcd8a 100644 --- a/homeassistant/components/tractive/switch.py +++ b/homeassistant/components/tractive/switch.py @@ -11,17 +11,15 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import Trackables +from . import Trackables, TractiveClient from .const import ( ATTR_BUZZER, ATTR_LED, ATTR_LIVE_TRACKING, CLIENT, DOMAIN, - SERVER_UNAVAILABLE, TRACKABLES, TRACKER_HARDWARE_STATUS_UPDATED, ) @@ -77,7 +75,7 @@ async def async_setup_entry( trackables = hass.data[DOMAIN][entry.entry_id][TRACKABLES] entities = [ - TractiveSwitch(client.user_id, item, description) + TractiveSwitch(client, item, description) for description in SWITCH_TYPES for item in trackables ] @@ -92,12 +90,17 @@ class TractiveSwitch(TractiveEntity, SwitchEntity): def __init__( self, - user_id: str, + client: TractiveClient, item: Trackables, description: TractiveSwitchEntityDescription, ) -> None: """Initialize switch entity.""" - super().__init__(user_id, item.trackable, item.tracker_details) + super().__init__( + client, + item.trackable, + item.tracker_details, + f"{TRACKER_HARDWARE_STATUS_UPDATED}-{item.tracker_details['_id']}", + ) self._attr_unique_id = f"{item.trackable['_id']}_{description.key}" self._attr_available = False @@ -106,38 +109,11 @@ def __init__( self.entity_description = description @callback - def handle_server_unavailable(self) -> None: - """Handle server unavailable.""" - self._attr_available = False - self.async_write_ha_state() + def handle_status_update(self, event: dict[str, Any]) -> None: + """Handle status update.""" + self._attr_is_on = event[self.entity_description.key] - @callback - def handle_hardware_status_update(self, event: dict[str, Any]) -> None: - """Handle hardware status update.""" - if (state := event[self.entity_description.key]) is None: - return - self._attr_is_on = state - self._attr_available = True - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """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, - ) - ) + super().handle_status_update(event) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on a switch."""