Adopt Hue integration to latest changes in Hue firmware (#101001)

pull/101386/head
Marcel van der Veldt 2023-09-27 23:36:12 +02:00 committed by Paulus Schoutsen
parent dde4b07c29
commit 415042f356
9 changed files with 216 additions and 29 deletions

View File

@ -11,6 +11,6 @@
"iot_class": "local_push",
"loggers": ["aiohue"],
"quality_scale": "platinum",
"requirements": ["aiohue==4.6.2"],
"requirements": ["aiohue==4.7.0"],
"zeroconf": ["_hue._tcp.local."]
}

View File

@ -1,7 +1,7 @@
"""Support for Hue binary sensors."""
from __future__ import annotations
from typing import Any, TypeAlias
from typing import TypeAlias
from aiohue.v2 import HueBridgeV2
from aiohue.v2.controllers.config import (
@ -9,9 +9,17 @@ from aiohue.v2.controllers.config import (
EntertainmentConfigurationController,
)
from aiohue.v2.controllers.events import EventType
from aiohue.v2.controllers.sensors import MotionController
from aiohue.v2.controllers.sensors import (
CameraMotionController,
ContactController,
MotionController,
TamperController,
)
from aiohue.v2.models.camera_motion import CameraMotion
from aiohue.v2.models.contact import Contact, ContactState
from aiohue.v2.models.entertainment_configuration import EntertainmentStatus
from aiohue.v2.models.motion import Motion
from aiohue.v2.models.tamper import Tamper, TamperState
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
@ -25,8 +33,16 @@ from ..bridge import HueBridge
from ..const import DOMAIN
from .entity import HueBaseEntity
SensorType: TypeAlias = Motion | EntertainmentConfiguration
ControllerType: TypeAlias = MotionController | EntertainmentConfigurationController
SensorType: TypeAlias = (
CameraMotion | Contact | Motion | EntertainmentConfiguration | Tamper
)
ControllerType: TypeAlias = (
CameraMotionController
| ContactController
| MotionController
| EntertainmentConfigurationController
| TamperController
)
async def async_setup_entry(
@ -57,8 +73,11 @@ async def async_setup_entry(
)
# setup for each binary-sensor-type hue resource
register_items(api.sensors.camera_motion, HueMotionSensor)
register_items(api.sensors.motion, HueMotionSensor)
register_items(api.config.entertainment_configuration, HueEntertainmentActiveSensor)
register_items(api.sensors.contact, HueContactSensor)
register_items(api.sensors.tamper, HueTamperSensor)
class HueBinarySensorBase(HueBaseEntity, BinarySensorEntity):
@ -87,12 +106,7 @@ class HueMotionSensor(HueBinarySensorBase):
if not self.resource.enabled:
# Force None (unknown) if the sensor is set to disabled in Hue
return None
return self.resource.motion.motion
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
return {"motion_valid": self.resource.motion.motion_valid}
return self.resource.motion.value
class HueEntertainmentActiveSensor(HueBinarySensorBase):
@ -110,3 +124,30 @@ class HueEntertainmentActiveSensor(HueBinarySensorBase):
"""Return sensor name."""
type_title = self.resource.type.value.replace("_", " ").title()
return f"{self.resource.metadata.name}: {type_title}"
class HueContactSensor(HueBinarySensorBase):
"""Representation of a Hue Contact sensor."""
_attr_device_class = BinarySensorDeviceClass.OPENING
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
if not self.resource.enabled:
# Force None (unknown) if the sensor is set to disabled in Hue
return None
return self.resource.contact_report.state != ContactState.CONTACT
class HueTamperSensor(HueBinarySensorBase):
"""Representation of a Hue Tamper sensor."""
_attr_device_class = BinarySensorDeviceClass.TAMPER
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
if not self.resource.tamper_reports:
return False
return self.resource.tamper_reports[0].state == TamperState.TAMPERED

View File

@ -100,12 +100,7 @@ class HueTemperatureSensor(HueSensorBase):
@property
def native_value(self) -> float:
"""Return the value reported by the sensor."""
return round(self.resource.temperature.temperature, 1)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
return {"temperature_valid": self.resource.temperature.temperature_valid}
return round(self.resource.temperature.value, 1)
class HueLightLevelSensor(HueSensorBase):
@ -122,14 +117,13 @@ class HueLightLevelSensor(HueSensorBase):
# scale used because the human eye adjusts to light levels and small
# changes at low lux levels are more noticeable than at high lux
# levels.
return int(10 ** ((self.resource.light.light_level - 1) / 10000))
return int(10 ** ((self.resource.light.value - 1) / 10000))
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
return {
"light_level": self.resource.light.light_level,
"light_level_valid": self.resource.light.light_level_valid,
"light_level": self.resource.light.value,
}
@ -149,6 +143,8 @@ class HueBatterySensor(HueSensorBase):
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
if self.resource.power_state.battery_state is None:
return {}
return {"battery_state": self.resource.power_state.battery_state.value}

View File

@ -256,7 +256,7 @@ aiohomekit==3.0.5
aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==4.6.2
aiohue==4.7.0
# homeassistant.components.imap
aioimaplib==1.0.1

View File

@ -234,7 +234,7 @@ aiohomekit==3.0.5
aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==4.6.2
aiohue==4.7.0
# homeassistant.components.imap
aioimaplib==1.0.1

View File

@ -2221,5 +2221,113 @@
"id": "52612630-841e-4d39-9763-60346a0da759",
"is_configured": true,
"type": "geolocation"
},
{
"id": "0240be0e-8b79-4a53-b9bb-b17fa14d7e75",
"product_data": {
"model_id": "SOC001",
"manufacturer_name": "Signify Netherlands B.V.",
"product_name": "Hue secure contact sensor",
"product_archetype": "unknown_archetype",
"certified": true,
"software_version": "2.67.9",
"hardware_platform_type": "100b-125"
},
"metadata": {
"name": "Test contact sensor",
"archetype": "unknown_archetype"
},
"identify": {},
"services": [
{
"rid": "18802b4a-b2f6-45dc-8813-99cde47f3a4a",
"rtype": "contact"
},
{
"rid": "d7fcfab0-69e1-4afb-99df-6ed505211db4",
"rtype": "tamper"
}
],
"type": "device"
},
{
"id": "18802b4a-b2f6-45dc-8813-99cde47f3a4a",
"owner": {
"rid": "0240be0e-8b79-4a53-b9bb-b17fa14d7e75",
"rtype": "device"
},
"enabled": true,
"contact_report": {
"changed": "2023-09-27T10:01:36.968Z",
"state": "contact"
},
"type": "contact"
},
{
"id": "d7fcfab0-69e1-4afb-99df-6ed505211db4",
"owner": {
"rid": "0240be0e-8b79-4a53-b9bb-b17fa14d7e75",
"rtype": "device"
},
"tamper_reports": [
{
"changed": "2023-09-25T10:02:08.774Z",
"source": "battery_door",
"state": "not_tampered"
}
],
"type": "tamper"
},
{
"id": "1cbda90c-b675-46b0-9e97-278e7e7857ed",
"id_v1": "/sensors/249",
"product_data": {
"model_id": "CAMERA",
"manufacturer_name": "Signify Netherlands B.V.",
"product_name": "Fake Hue Test Camera",
"product_archetype": "unknown_archetype",
"certified": true,
"software_version": "0.0.0",
"hardware_platform_type": "0"
},
"metadata": {
"name": "Test Camera",
"archetype": "unknown_archetype"
},
"identify": {},
"usertest": {
"status": "set",
"usertest": false
},
"services": [
{
"rid": "d9f2cfee-5879-426b-aa1f-553af8f38176",
"rtype": "camera_motion"
}
],
"type": "device"
},
{
"id": "d9f2cfee-5879-426b-aa1f-553af8f38176",
"id_v1": "/sensors/249",
"owner": {
"rid": "1cbda90c-b675-46b0-9e97-278e7e7857ed",
"rtype": "device"
},
"enabled": true,
"motion": {
"motion": true,
"motion_valid": true,
"motion_report": {
"changed": "2023-09-27T10:06:41.822Z",
"motion": true
}
},
"sensitivity": {
"status": "set",
"sensitivity": 2,
"sensitivity_max": 4
},
"type": "motion"
}
]

View File

@ -14,8 +14,8 @@ async def test_binary_sensors(
await setup_platform(hass, mock_bridge_v2, "binary_sensor")
# there shouldn't have been any requests at this point
assert len(mock_bridge_v2.mock_requests) == 0
# 2 binary_sensors should be created from test data
assert len(hass.states.async_all()) == 2
# 5 binary_sensors should be created from test data
assert len(hass.states.async_all()) == 5
# test motion sensor
sensor = hass.states.get("binary_sensor.hue_motion_sensor_motion")
@ -23,7 +23,6 @@ async def test_binary_sensors(
assert sensor.state == "off"
assert sensor.name == "Hue motion sensor Motion"
assert sensor.attributes["device_class"] == "motion"
assert sensor.attributes["motion_valid"] is True
# test entertainment room active sensor
sensor = hass.states.get(
@ -34,6 +33,51 @@ async def test_binary_sensors(
assert sensor.name == "Entertainmentroom 1: Entertainment Configuration"
assert sensor.attributes["device_class"] == "running"
# test contact sensor
sensor = hass.states.get("binary_sensor.test_contact_sensor_contact")
assert sensor is not None
assert sensor.state == "off"
assert sensor.name == "Test contact sensor Contact"
assert sensor.attributes["device_class"] == "opening"
# test contact sensor disabled == state unknown
mock_bridge_v2.api.emit_event(
"update",
{
"enabled": False,
"id": "18802b4a-b2f6-45dc-8813-99cde47f3a4a",
"type": "contact",
},
)
await hass.async_block_till_done()
sensor = hass.states.get("binary_sensor.test_contact_sensor_contact")
assert sensor.state == "unknown"
# test tamper sensor
sensor = hass.states.get("binary_sensor.test_contact_sensor_tamper")
assert sensor is not None
assert sensor.state == "off"
assert sensor.name == "Test contact sensor Tamper"
assert sensor.attributes["device_class"] == "tamper"
# test tamper sensor when no tamper reports exist
mock_bridge_v2.api.emit_event(
"update",
{
"id": "d7fcfab0-69e1-4afb-99df-6ed505211db4",
"tamper_reports": [],
"type": "tamper",
},
)
await hass.async_block_till_done()
sensor = hass.states.get("binary_sensor.test_contact_sensor_tamper")
assert sensor.state == "off"
# test camera_motion sensor
sensor = hass.states.get("binary_sensor.test_camera_motion")
assert sensor is not None
assert sensor.state == "on"
assert sensor.name == "Test Camera Motion"
assert sensor.attributes["device_class"] == "motion"
async def test_binary_sensor_add_update(hass: HomeAssistant, mock_bridge_v2) -> None:
"""Test if binary_sensor get added/updated from events."""

View File

@ -28,7 +28,6 @@ async def test_sensors(
assert sensor.attributes["device_class"] == "temperature"
assert sensor.attributes["state_class"] == "measurement"
assert sensor.attributes["unit_of_measurement"] == "°C"
assert sensor.attributes["temperature_valid"] is True
# test illuminance sensor
sensor = hass.states.get("sensor.hue_motion_sensor_illuminance")
@ -39,7 +38,6 @@ async def test_sensors(
assert sensor.attributes["state_class"] == "measurement"
assert sensor.attributes["unit_of_measurement"] == "lx"
assert sensor.attributes["light_level"] == 18027
assert sensor.attributes["light_level_valid"] is True
# test battery sensor
sensor = hass.states.get("sensor.wall_switch_with_2_controls_battery")

View File

@ -14,8 +14,8 @@ async def test_switch(
await setup_platform(hass, mock_bridge_v2, "switch")
# there shouldn't have been any requests at this point
assert len(mock_bridge_v2.mock_requests) == 0
# 2 entities should be created from test data
assert len(hass.states.async_all()) == 2
# 3 entities should be created from test data
assert len(hass.states.async_all()) == 3
# test config switch to enable/disable motion sensor
test_entity = hass.states.get("switch.hue_motion_sensor_motion")