Parcourir la source

Add entity translations and entity_category, decouple UniFi sensor availability

Every sensor/binary_sensor/switch/button now uses _attr_translation_key
with a corresponding entity section in strings.json/translations/en.json
instead of a bare _attr_name, and config/diagnostic-shaped entities are
tagged with EntityCategory.CONFIG/DIAGNOSTIC.

UnifiAvailableSensor's state comes from the unifi integration, not from
RoomCalCoordinator, so its availability should not follow the coordinator's
last_update_success — override available to always return True.
ZiB il y a 3 mois
Parent
commit
d146b980fe

+ 19 - 5
custom_components/room_cal_ha/binary_sensor.py

@@ -4,7 +4,7 @@ from datetime import timedelta
 
 
 from homeassistant.components.binary_sensor import BinarySensorEntity
 from homeassistant.components.binary_sensor import BinarySensorEntity
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import EVENT_COMPONENT_LOADED
+from homeassistant.const import EVENT_COMPONENT_LOADED, EntityCategory
 from homeassistant.core import Event, HomeAssistant, callback
 from homeassistant.core import Event, HomeAssistant, callback
 from homeassistant.helpers.device_registry import DeviceInfo
 from homeassistant.helpers.device_registry import DeviceInfo
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -50,8 +50,9 @@ async def async_setup_entry(
 
 
 class StaleSensor(CoordinatorEntity[RoomCalCoordinator], BinarySensorEntity):
 class StaleSensor(CoordinatorEntity[RoomCalCoordinator], BinarySensorEntity):
   _attr_has_entity_name = True
   _attr_has_entity_name = True
-  _attr_name = "Stale"
+  _attr_translation_key = "stale"
   _attr_icon = "mdi:clock-alert-outline"
   _attr_icon = "mdi:clock-alert-outline"
+  _attr_entity_category = EntityCategory.DIAGNOSTIC
 
 
   def __init__(
   def __init__(
     self,
     self,
@@ -82,9 +83,18 @@ class StaleSensor(CoordinatorEntity[RoomCalCoordinator], BinarySensorEntity):
 
 
 
 
 class UnifiAvailableSensor(CoordinatorEntity[RoomCalCoordinator], BinarySensorEntity):
 class UnifiAvailableSensor(CoordinatorEntity[RoomCalCoordinator], BinarySensorEntity):
+  """Reflects UniFi hub availability, independent of the room_cal_ha coordinator.
+
+  This entity's state comes entirely from the `unifi` integration's config entries,
+  not from `coordinator.data` — so its availability must not be tied to
+  `coordinator.last_update_success` (the CoordinatorEntity default). A failed room_cal
+  backend poll has no bearing on whether UniFi itself is reachable.
+  """
+
   _attr_has_entity_name = True
   _attr_has_entity_name = True
-  _attr_name = "UniFi"
+  _attr_translation_key = "unifi_available"
   _attr_icon = "mdi:wifi-check"
   _attr_icon = "mdi:wifi-check"
+  _attr_entity_category = EntityCategory.DIAGNOSTIC
 
 
   def __init__(
   def __init__(
     self,
     self,
@@ -108,6 +118,10 @@ class UnifiAvailableSensor(CoordinatorEntity[RoomCalCoordinator], BinarySensorEn
     if event.data.get("component") == UNIFI_DOMAIN:
     if event.data.get("component") == UNIFI_DOMAIN:
       self.async_write_ha_state()
       self.async_write_ha_state()
 
 
+  @property
+  def available(self) -> bool:
+    return True
+
   @property
   @property
   def is_on(self) -> bool:
   def is_on(self) -> bool:
     for config_entry in self.hass.config_entries.async_loaded_entries(UNIFI_DOMAIN):
     for config_entry in self.hass.config_entries.async_loaded_entries(UNIFI_DOMAIN):
@@ -119,7 +133,7 @@ class UnifiAvailableSensor(CoordinatorEntity[RoomCalCoordinator], BinarySensorEn
 
 
 class ArrivedSensor(CoordinatorEntity[RoomCalCoordinator], BinarySensorEntity):
 class ArrivedSensor(CoordinatorEntity[RoomCalCoordinator], BinarySensorEntity):
   _attr_has_entity_name = True
   _attr_has_entity_name = True
-  _attr_name = "Arrived"
+  _attr_translation_key = "arrived"
   _attr_icon = "mdi:account-check"
   _attr_icon = "mdi:account-check"
 
 
   def __init__(
   def __init__(
@@ -139,7 +153,7 @@ class ArrivedSensor(CoordinatorEntity[RoomCalCoordinator], BinarySensorEntity):
 
 
 class DepartedSensor(CoordinatorEntity[RoomCalCoordinator], BinarySensorEntity):
 class DepartedSensor(CoordinatorEntity[RoomCalCoordinator], BinarySensorEntity):
   _attr_has_entity_name = True
   _attr_has_entity_name = True
-  _attr_name = "Departed"
+  _attr_translation_key = "departed"
   _attr_icon = "mdi:account-arrow-left"
   _attr_icon = "mdi:account-arrow-left"
 
 
   def __init__(
   def __init__(

+ 9 - 7
custom_components/room_cal_ha/button.py

@@ -4,6 +4,7 @@ from datetime import datetime, timedelta
 
 
 from homeassistant.components.button import ButtonEntity
 from homeassistant.components.button import ButtonEntity
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import EntityCategory
 from homeassistant.core import HomeAssistant
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.device_registry import DeviceInfo
 from homeassistant.helpers.device_registry import DeviceInfo
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -35,7 +36,8 @@ async def async_setup_entry(
 
 
 class ForceRefreshButton(ButtonEntity):
 class ForceRefreshButton(ButtonEntity):
   _attr_has_entity_name = True
   _attr_has_entity_name = True
-  _attr_name = "Force Refresh"
+  _attr_translation_key = "force_refresh"
+  _attr_entity_category = EntityCategory.DIAGNOSTIC
 
 
   def __init__(
   def __init__(
     self,
     self,
@@ -56,7 +58,7 @@ class ForceRefreshButton(ButtonEntity):
 
 
 class PromoteFromNextButton(ButtonEntity):
 class PromoteFromNextButton(ButtonEntity):
   _attr_has_entity_name = True
   _attr_has_entity_name = True
-  _attr_name = "Promote from Next"
+  _attr_translation_key = "promote_from_next"
 
 
   def __init__(
   def __init__(
     self,
     self,
@@ -77,7 +79,7 @@ class PromoteFromNextButton(ButtonEntity):
 
 
 class ClearCurrentButton(ButtonEntity):
 class ClearCurrentButton(ButtonEntity):
   _attr_has_entity_name = True
   _attr_has_entity_name = True
-  _attr_name = "Clear Current"
+  _attr_translation_key = "clear_current"
 
 
   def __init__(
   def __init__(
     self,
     self,
@@ -98,7 +100,7 @@ class ClearCurrentButton(ButtonEntity):
 
 
 class TestNextGuestButton(CoordinatorEntity[RoomCalCoordinator], ButtonEntity):
 class TestNextGuestButton(CoordinatorEntity[RoomCalCoordinator], ButtonEntity):
   _attr_has_entity_name = True
   _attr_has_entity_name = True
-  _attr_name = "Test Next Guest"
+  _attr_translation_key = "test_next_guest"
   _attr_icon = "mdi:test-tube"
   _attr_icon = "mdi:test-tube"
   __test__ = False  # prevent pytest from collecting this as a test class
   __test__ = False  # prevent pytest from collecting this as a test class
 
 
@@ -129,7 +131,7 @@ class TestNextGuestButton(CoordinatorEntity[RoomCalCoordinator], ButtonEntity):
 
 
 class OverrideEmptyButton(CoordinatorEntity[RoomCalCoordinator], ButtonEntity):
 class OverrideEmptyButton(CoordinatorEntity[RoomCalCoordinator], ButtonEntity):
   _attr_has_entity_name = True
   _attr_has_entity_name = True
-  _attr_name = "Override Empty"
+  _attr_translation_key = "override_empty"
   _attr_icon = "mdi:calendar-remove"
   _attr_icon = "mdi:calendar-remove"
 
 
   def __init__(
   def __init__(
@@ -155,7 +157,7 @@ class OverrideEmptyButton(CoordinatorEntity[RoomCalCoordinator], ButtonEntity):
 
 
 class GuestArrivedButton(ButtonEntity):
 class GuestArrivedButton(ButtonEntity):
   _attr_has_entity_name = True
   _attr_has_entity_name = True
-  _attr_name = "Guest Arrived"
+  _attr_translation_key = "guest_arrived"
   _attr_icon = "mdi:account-arrow-right"
   _attr_icon = "mdi:account-arrow-right"
 
 
   def __init__(
   def __init__(
@@ -177,7 +179,7 @@ class GuestArrivedButton(ButtonEntity):
 
 
 class GuestDepartedButton(ButtonEntity):
 class GuestDepartedButton(ButtonEntity):
   _attr_has_entity_name = True
   _attr_has_entity_name = True
-  _attr_name = "Guest Departed"
+  _attr_translation_key = "guest_departed"
   _attr_icon = "mdi:account-arrow-left"
   _attr_icon = "mdi:account-arrow-left"
 
 
   def __init__(
   def __init__(

+ 17 - 13
custom_components/room_cal_ha/sensor.py

@@ -5,6 +5,7 @@ from datetime import date, datetime, time
 
 
 from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
 from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import EntityCategory
 from homeassistant.core import HomeAssistant
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.device_registry import DeviceInfo
 from homeassistant.helpers.device_registry import DeviceInfo
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -57,14 +58,14 @@ async def async_setup_entry(
       name=device_name,
       name=device_name,
     )
     )
     entities += [
     entities += [
-      GuestNameSensor(coordinator, entry, slot, device_info, "Guest Name", "guest_name", "mdi:account-convert"),
-      ReservationDateTimeSensor(coordinator, entry, slot, device_info, "Arrival", "check_in_date", "check_in_time", "mdi:calendar-start"),
-      ReservationTimeSensor(coordinator, entry, slot, device_info, "Check In", "check_in_time", "mdi:timer-check-outline"),
-      ReservationDateTimeSensor(coordinator, entry, slot, device_info, "Departure", "check_out_date", "check_out_time", "mdi:calendar-end"),
-      ReservationTimeSensor(coordinator, entry, slot, device_info, "Check Out", "check_out_time", "mdi:timer-cancel-outline"),
-      ReservationStrSensor(coordinator, entry, slot, device_info, "WiFi SSID", "wifi_ssid", "mdi:wifi"),
+      GuestNameSensor(coordinator, entry, slot, device_info, "guest_name", "guest_name", "mdi:account-convert"),
+      ReservationDateTimeSensor(coordinator, entry, slot, device_info, "arrival", "check_in_date", "check_in_time", "mdi:calendar-start"),
+      ReservationTimeSensor(coordinator, entry, slot, device_info, "check_in", "check_in_time", "mdi:timer-check-outline"),
+      ReservationDateTimeSensor(coordinator, entry, slot, device_info, "departure", "check_out_date", "check_out_time", "mdi:calendar-end"),
+      ReservationTimeSensor(coordinator, entry, slot, device_info, "check_out", "check_out_time", "mdi:timer-cancel-outline"),
+      ReservationStrSensor(coordinator, entry, slot, device_info, "wifi_ssid", "wifi_ssid", "mdi:wifi"),
       WiFiKeySensor(coordinator, entry, slot, device_info),
       WiFiKeySensor(coordinator, entry, slot, device_info),
-      ReservationStrSensor(coordinator, entry, slot, device_info, "Door Code", "door_code", "mdi:door-closed-lock"),
+      ReservationStrSensor(coordinator, entry, slot, device_info, "door_code", "door_code", "mdi:door-closed-lock"),
       LastUpdatedSensor(coordinator, entry, slot, device_info),
       LastUpdatedSensor(coordinator, entry, slot, device_info),
     ]
     ]
 
 
@@ -73,8 +74,9 @@ async def async_setup_entry(
 
 
 class SyncSourceSensor(SensorEntity):
 class SyncSourceSensor(SensorEntity):
   _attr_has_entity_name = True
   _attr_has_entity_name = True
-  _attr_name = "Sync Source"
+  _attr_translation_key = "sync_source"
   _attr_icon = "mdi:sync"
   _attr_icon = "mdi:sync"
+  _attr_entity_category = EntityCategory.DIAGNOSTIC
 
 
   def __init__(self, entry: ConfigEntry, room_name: str, device_info: DeviceInfo) -> None:
   def __init__(self, entry: ConfigEntry, room_name: str, device_info: DeviceInfo) -> None:
     self._entry = entry
     self._entry = entry
@@ -106,14 +108,14 @@ class _ReservationSensor(CoordinatorEntity[RoomCalCoordinator], SensorEntity):
     entry: ConfigEntry,
     entry: ConfigEntry,
     slot: str,
     slot: str,
     device_info: DeviceInfo,
     device_info: DeviceInfo,
-    name: str,
+    translation_key: str,
     field: str,
     field: str,
     icon: str | None = None,
     icon: str | None = None,
   ) -> None:
   ) -> None:
     super().__init__(coordinator)
     super().__init__(coordinator)
     self._slot = slot
     self._slot = slot
     self._field = field
     self._field = field
-    self._attr_name = name
+    self._attr_translation_key = translation_key
     self._attr_unique_id = f"{entry.entry_id}_{slot}_{field}"
     self._attr_unique_id = f"{entry.entry_id}_{slot}_{field}"
     self._attr_device_info = device_info
     self._attr_device_info = device_info
     self._attr_icon = icon
     self._attr_icon = icon
@@ -152,7 +154,7 @@ class GuestNameSensor(ReservationStrSensor):
 
 
 class WiFiKeySensor(_ReservationSensor):
 class WiFiKeySensor(_ReservationSensor):
   def __init__(self, coordinator, entry, slot, device_info) -> None:
   def __init__(self, coordinator, entry, slot, device_info) -> None:
-    super().__init__(coordinator, entry, slot, device_info, "WiFi Key", "wifi_key", "mdi:wifi-lock")
+    super().__init__(coordinator, entry, slot, device_info, "wifi_key", "wifi_key", "mdi:wifi-lock")
 
 
   @property
   @property
   def native_value(self) -> str | None:
   def native_value(self) -> str | None:
@@ -229,9 +231,10 @@ class ReservationDateTimeSensor(_ReservationSensor):
 
 
 class LastUpdatedSensor(CoordinatorEntity[RoomCalCoordinator], SensorEntity):
 class LastUpdatedSensor(CoordinatorEntity[RoomCalCoordinator], SensorEntity):
   _attr_has_entity_name = True
   _attr_has_entity_name = True
-  _attr_name = "Last Updated"
+  _attr_translation_key = "last_updated"
   _attr_device_class = SensorDeviceClass.TIMESTAMP
   _attr_device_class = SensorDeviceClass.TIMESTAMP
   _attr_icon = "mdi:clock-edit-outline"
   _attr_icon = "mdi:clock-edit-outline"
+  _attr_entity_category = EntityCategory.DIAGNOSTIC
 
 
   def __init__(
   def __init__(
     self,
     self,
@@ -267,8 +270,9 @@ class LastUpdatedSensor(CoordinatorEntity[RoomCalCoordinator], SensorEntity):
 
 
 class ApiHealthSensor(CoordinatorEntity[RoomCalCoordinator], SensorEntity):
 class ApiHealthSensor(CoordinatorEntity[RoomCalCoordinator], SensorEntity):
   _attr_has_entity_name = True
   _attr_has_entity_name = True
-  _attr_name = "API Health"
+  _attr_translation_key = "api_health"
   _attr_icon = "mdi:api"
   _attr_icon = "mdi:api"
+  _attr_entity_category = EntityCategory.DIAGNOSTIC
 
 
   def __init__(
   def __init__(
     self,
     self,

+ 36 - 0
custom_components/room_cal_ha/strings.json

@@ -92,6 +92,42 @@
       "invalid_time": "Enter a valid time in HH:MM format, or leave blank."
       "invalid_time": "Enter a valid time in HH:MM format, or leave blank."
     }
     }
   },
   },
+  "entity": {
+    "sensor": {
+      "sync_source": { "name": "Sync Source" },
+      "guest_name": { "name": "Guest Name" },
+      "arrival": { "name": "Arrival" },
+      "check_in": { "name": "Check In" },
+      "departure": { "name": "Departure" },
+      "check_out": { "name": "Check Out" },
+      "wifi_ssid": { "name": "WiFi SSID" },
+      "wifi_key": { "name": "WiFi Key" },
+      "door_code": { "name": "Door Code" },
+      "last_updated": { "name": "Last Updated" },
+      "api_health": { "name": "API Health" }
+    },
+    "binary_sensor": {
+      "stale": { "name": "Stale" },
+      "unifi_available": { "name": "UniFi" },
+      "arrived": { "name": "Arrived" },
+      "departed": { "name": "Departed" }
+    },
+    "switch": {
+      "mode_sync": { "name": "Mode Sync" },
+      "use_unifi": { "name": "Use UniFi" },
+      "use_zlock_slots": { "name": "Use zLock Slots" },
+      "override_reservation": { "name": "Override Reservation" }
+    },
+    "button": {
+      "force_refresh": { "name": "Force Refresh" },
+      "promote_from_next": { "name": "Promote from Next" },
+      "clear_current": { "name": "Clear Current" },
+      "test_next_guest": { "name": "Test Next Guest" },
+      "override_empty": { "name": "Override Empty" },
+      "guest_arrived": { "name": "Guest Arrived" },
+      "guest_departed": { "name": "Guest Departed" }
+    }
+  },
   "services": {
   "services": {
     "update_reservation": {
     "update_reservation": {
       "name": "Update Reservation",
       "name": "Update Reservation",

+ 8 - 4
custom_components/room_cal_ha/switch.py

@@ -2,6 +2,7 @@ from __future__ import annotations
 
 
 from homeassistant.components.switch import SwitchEntity
 from homeassistant.components.switch import SwitchEntity
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import EntityCategory
 from homeassistant.core import HomeAssistant
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.device_registry import DeviceInfo
 from homeassistant.helpers.device_registry import DeviceInfo
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -89,7 +90,8 @@ class RoomStateSwitch(_SwitchBase):
 
 
 
 
 class ModeSyncSwitch(_SwitchBase):
 class ModeSyncSwitch(_SwitchBase):
-  _attr_name = "Mode Sync"
+  _attr_translation_key = "mode_sync"
+  _attr_entity_category = EntityCategory.CONFIG
 
 
   def __init__(self, coordinator: RoomCalCoordinator, entry: ConfigEntry, device_name: str) -> None:
   def __init__(self, coordinator: RoomCalCoordinator, entry: ConfigEntry, device_name: str) -> None:
     super().__init__(coordinator, entry, device_name)
     super().__init__(coordinator, entry, device_name)
@@ -105,8 +107,9 @@ class ModeSyncSwitch(_SwitchBase):
 
 
 
 
 class UseUnifiSwitch(_SwitchBase):
 class UseUnifiSwitch(_SwitchBase):
-  _attr_name = "Use UniFi"
+  _attr_translation_key = "use_unifi"
   _attr_icon = "mdi:wifi-settings"
   _attr_icon = "mdi:wifi-settings"
+  _attr_entity_category = EntityCategory.CONFIG
   _state: bool = True  # default on; base async_added_to_hass restores saved state
   _state: bool = True  # default on; base async_added_to_hass restores saved state
 
 
   def __init__(self, coordinator: RoomCalCoordinator, entry: ConfigEntry, device_name: str) -> None:
   def __init__(self, coordinator: RoomCalCoordinator, entry: ConfigEntry, device_name: str) -> None:
@@ -115,8 +118,9 @@ class UseUnifiSwitch(_SwitchBase):
 
 
 
 
 class UseZlockSlotsSwitch(_SwitchBase):
 class UseZlockSlotsSwitch(_SwitchBase):
-  _attr_name = "Use zLock Slots"
+  _attr_translation_key = "use_zlock_slots"
   _attr_icon = "mdi:lock-smart"
   _attr_icon = "mdi:lock-smart"
+  _attr_entity_category = EntityCategory.CONFIG
   _state: bool = False  # default off until zlock entries are configured
   _state: bool = False  # default off until zlock entries are configured
 
 
   def __init__(self, coordinator: RoomCalCoordinator, entry: ConfigEntry, device_name: str) -> None:
   def __init__(self, coordinator: RoomCalCoordinator, entry: ConfigEntry, device_name: str) -> None:
@@ -130,7 +134,7 @@ class UseZlockSlotsSwitch(_SwitchBase):
 
 
 class OverrideReservationSwitch(CoordinatorEntity[RoomCalCoordinator], SwitchEntity):
 class OverrideReservationSwitch(CoordinatorEntity[RoomCalCoordinator], SwitchEntity):
   _attr_has_entity_name = True
   _attr_has_entity_name = True
-  _attr_name = "Override Reservation"
+  _attr_translation_key = "override_reservation"
   _attr_icon = "mdi:test-tube"
   _attr_icon = "mdi:test-tube"
 
 
   def __init__(self, coordinator: RoomCalCoordinator, entry: ConfigEntry, device_name: str) -> None:
   def __init__(self, coordinator: RoomCalCoordinator, entry: ConfigEntry, device_name: str) -> None:

+ 36 - 0
custom_components/room_cal_ha/translations/en.json

@@ -92,6 +92,42 @@
       "invalid_time": "Enter a valid time in HH:MM format, or leave blank."
       "invalid_time": "Enter a valid time in HH:MM format, or leave blank."
     }
     }
   },
   },
+  "entity": {
+    "sensor": {
+      "sync_source": { "name": "Sync Source" },
+      "guest_name": { "name": "Guest Name" },
+      "arrival": { "name": "Arrival" },
+      "check_in": { "name": "Check In" },
+      "departure": { "name": "Departure" },
+      "check_out": { "name": "Check Out" },
+      "wifi_ssid": { "name": "WiFi SSID" },
+      "wifi_key": { "name": "WiFi Key" },
+      "door_code": { "name": "Door Code" },
+      "last_updated": { "name": "Last Updated" },
+      "api_health": { "name": "API Health" }
+    },
+    "binary_sensor": {
+      "stale": { "name": "Stale" },
+      "unifi_available": { "name": "UniFi" },
+      "arrived": { "name": "Arrived" },
+      "departed": { "name": "Departed" }
+    },
+    "switch": {
+      "mode_sync": { "name": "Mode Sync" },
+      "use_unifi": { "name": "Use UniFi" },
+      "use_zlock_slots": { "name": "Use zLock Slots" },
+      "override_reservation": { "name": "Override Reservation" }
+    },
+    "button": {
+      "force_refresh": { "name": "Force Refresh" },
+      "promote_from_next": { "name": "Promote from Next" },
+      "clear_current": { "name": "Clear Current" },
+      "test_next_guest": { "name": "Test Next Guest" },
+      "override_empty": { "name": "Override Empty" },
+      "guest_arrived": { "name": "Guest Arrived" },
+      "guest_departed": { "name": "Guest Departed" }
+    }
+  },
   "services": {
   "services": {
     "update_reservation": {
     "update_reservation": {
       "name": "Update Reservation",
       "name": "Update Reservation",