Add support for binary sensor states in Google Assistant (#127652)

This commit is contained in:
Joost Lekkerkerker 2024-11-10 20:34:24 +01:00 committed by GitHub
parent 784ad20fb6
commit f7f1830b7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 182 additions and 33 deletions

View File

@ -78,6 +78,7 @@ TYPE_AWNING = f"{PREFIX_TYPES}AWNING"
TYPE_BLINDS = f"{PREFIX_TYPES}BLINDS"
TYPE_CAMERA = f"{PREFIX_TYPES}CAMERA"
TYPE_CURTAIN = f"{PREFIX_TYPES}CURTAIN"
TYPE_CARBON_MONOXIDE_DETECTOR = f"{PREFIX_TYPES}CARBON_MONOXIDE_DETECTOR"
TYPE_DEHUMIDIFIER = f"{PREFIX_TYPES}DEHUMIDIFIER"
TYPE_DOOR = f"{PREFIX_TYPES}DOOR"
TYPE_DOORBELL = f"{PREFIX_TYPES}DOORBELL"
@ -93,6 +94,7 @@ TYPE_SCENE = f"{PREFIX_TYPES}SCENE"
TYPE_SENSOR = f"{PREFIX_TYPES}SENSOR"
TYPE_SETTOP = f"{PREFIX_TYPES}SETTOP"
TYPE_SHUTTER = f"{PREFIX_TYPES}SHUTTER"
TYPE_SMOKE_DETECTOR = f"{PREFIX_TYPES}SMOKE_DETECTOR"
TYPE_SPEAKER = f"{PREFIX_TYPES}SPEAKER"
TYPE_SWITCH = f"{PREFIX_TYPES}SWITCH"
TYPE_THERMOSTAT = f"{PREFIX_TYPES}THERMOSTAT"
@ -136,6 +138,7 @@ EVENT_SYNC_RECEIVED = "google_assistant_sync"
DOMAIN_TO_GOOGLE_TYPES = {
alarm_control_panel.DOMAIN: TYPE_ALARM,
binary_sensor.DOMAIN: TYPE_SENSOR,
button.DOMAIN: TYPE_SCENE,
camera.DOMAIN: TYPE_CAMERA,
climate.DOMAIN: TYPE_THERMOSTAT,
@ -168,6 +171,14 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = {
binary_sensor.DOMAIN,
binary_sensor.BinarySensorDeviceClass.GARAGE_DOOR,
): TYPE_GARAGE,
(
binary_sensor.DOMAIN,
binary_sensor.BinarySensorDeviceClass.SMOKE,
): TYPE_SMOKE_DETECTOR,
(
binary_sensor.DOMAIN,
binary_sensor.BinarySensorDeviceClass.CO,
): TYPE_CARBON_MONOXIDE_DETECTOR,
(cover.DOMAIN, cover.CoverDeviceClass.AWNING): TYPE_AWNING,
(cover.DOMAIN, cover.CoverDeviceClass.CURTAIN): TYPE_CURTAIN,
(cover.DOMAIN, cover.CoverDeviceClass.DOOR): TYPE_DOOR,

View File

@ -2706,6 +2706,21 @@ class SensorStateTrait(_Trait):
),
}
binary_sensor_types = {
binary_sensor.BinarySensorDeviceClass.CO: (
"CarbonMonoxideLevel",
["carbon monoxide detected", "no carbon monoxide detected", "unknown"],
),
binary_sensor.BinarySensorDeviceClass.SMOKE: (
"SmokeLevel",
["smoke detected", "no smoke detected", "unknown"],
),
binary_sensor.BinarySensorDeviceClass.MOISTURE: (
"WaterLeak",
["leak", "no leak", "unknown"],
),
}
name = TRAIT_SENSOR_STATE
commands: list[str] = []
@ -2728,24 +2743,37 @@ class SensorStateTrait(_Trait):
@classmethod
def supported(cls, domain, features, device_class, _):
"""Test if state is supported."""
return domain == sensor.DOMAIN and device_class in cls.sensor_types
return (domain == sensor.DOMAIN and device_class in cls.sensor_types) or (
domain == binary_sensor.DOMAIN and device_class in cls.binary_sensor_types
)
def sync_attributes(self) -> dict[str, Any]:
"""Return attributes for a sync request."""
device_class = self.state.attributes.get(ATTR_DEVICE_CLASS)
data = self.sensor_types.get(device_class)
if device_class is None or data is None:
return {}
def create_sensor_state(
name: str,
raw_value_unit: str | None = None,
available_states: list[str] | None = None,
) -> dict[str, Any]:
sensor_state: dict[str, Any] = {
"name": name,
}
if raw_value_unit:
sensor_state["numericCapabilities"] = {"rawValueUnit": raw_value_unit}
if available_states:
sensor_state["descriptiveCapabilities"] = {
"availableStates": available_states
}
return {"sensorStatesSupported": [sensor_state]}
sensor_state = {
"name": data[0],
"numericCapabilities": {"rawValueUnit": data[1]},
}
if device_class == sensor.SensorDeviceClass.AQI:
sensor_state["descriptiveCapabilities"] = {
"availableStates": [
if self.state.domain == sensor.DOMAIN:
sensor_data = self.sensor_types.get(device_class)
if device_class is None or sensor_data is None:
return {}
available_states: list[str] | None = None
if device_class == sensor.SensorDeviceClass.AQI:
available_states = [
"healthy",
"moderate",
"unhealthy for sensitive groups",
@ -2753,30 +2781,53 @@ class SensorStateTrait(_Trait):
"very unhealthy",
"hazardous",
"unknown",
],
}
return {"sensorStatesSupported": [sensor_state]}
]
return create_sensor_state(sensor_data[0], sensor_data[1], available_states)
binary_sensor_data = self.binary_sensor_types.get(device_class)
if device_class is None or binary_sensor_data is None:
return {}
return create_sensor_state(
binary_sensor_data[0], available_states=binary_sensor_data[1]
)
def query_attributes(self) -> dict[str, Any]:
"""Return the attributes of this trait for this entity."""
device_class = self.state.attributes.get(ATTR_DEVICE_CLASS)
data = self.sensor_types.get(device_class)
if device_class is None or data is None:
def create_sensor_state(
name: str, raw_value: float | None = None, current_state: str | None = None
) -> dict[str, Any]:
sensor_state: dict[str, Any] = {
"name": name,
"rawValue": raw_value,
}
if current_state:
sensor_state["currentSensorState"] = current_state
return {"currentSensorStateData": [sensor_state]}
if self.state.domain == sensor.DOMAIN:
sensor_data = self.sensor_types.get(device_class)
if device_class is None or sensor_data is None:
return {}
try:
value = float(self.state.state)
except ValueError:
value = None
if self.state.state == STATE_UNKNOWN:
value = None
current_state: str | None = None
if device_class == sensor.SensorDeviceClass.AQI:
current_state = self._air_quality_description_for_aqi(value)
return create_sensor_state(sensor_data[0], value, current_state)
binary_sensor_data = self.binary_sensor_types.get(device_class)
if device_class is None or binary_sensor_data is None:
return {}
try:
value = float(self.state.state)
except ValueError:
value = None
if self.state.state == STATE_UNKNOWN:
value = None
sensor_data = {"name": data[0], "rawValue": value}
if device_class == sensor.SensorDeviceClass.AQI:
sensor_data["currentSensorState"] = self._air_quality_description_for_aqi(
value
)
return {"currentSensorStateData": [sensor_data]}
value = {
STATE_ON: 0,
STATE_OFF: 1,
STATE_UNKNOWN: 2,
}[self.state.state]
return create_sensor_state(
binary_sensor_data[0], current_state=binary_sensor_data[1][value]
)

View File

@ -4069,3 +4069,90 @@ async def test_sensorstate(
)
is False
)
@pytest.mark.parametrize(
("state", "identifier"),
[
(STATE_ON, 0),
(STATE_OFF, 1),
(STATE_UNKNOWN, 2),
],
)
@pytest.mark.parametrize(
("device_class", "name", "states"),
[
(
binary_sensor.BinarySensorDeviceClass.CO,
"CarbonMonoxideLevel",
["carbon monoxide detected", "no carbon monoxide detected", "unknown"],
),
(
binary_sensor.BinarySensorDeviceClass.SMOKE,
"SmokeLevel",
["smoke detected", "no smoke detected", "unknown"],
),
(
binary_sensor.BinarySensorDeviceClass.MOISTURE,
"WaterLeak",
["leak", "no leak", "unknown"],
),
],
)
async def test_binary_sensorstate(
hass: HomeAssistant,
state: str,
identifier: int,
device_class: binary_sensor.BinarySensorDeviceClass,
name: str,
states: list[str],
) -> None:
"""Test SensorState trait support for binary sensor domain."""
assert helpers.get_google_type(binary_sensor.DOMAIN, None) is not None
assert trait.SensorStateTrait.supported(
binary_sensor.DOMAIN, None, device_class, None
)
trt = trait.SensorStateTrait(
hass,
State(
"binary_sensor.test",
state,
{
"device_class": device_class,
},
),
BASIC_CONFIG,
)
assert trt.sync_attributes() == {
"sensorStatesSupported": [
{
"name": name,
"descriptiveCapabilities": {
"availableStates": states,
},
}
]
}
assert trt.query_attributes() == {
"currentSensorStateData": [
{
"name": name,
"currentSensorState": states[identifier],
"rawValue": None,
},
]
}
assert helpers.get_google_type(binary_sensor.DOMAIN, None) is not None
assert (
trait.SensorStateTrait.supported(
binary_sensor.DOMAIN,
None,
binary_sensor.BinarySensorDeviceClass.TAMPER,
None,
)
is False
)