mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
Add support for binary sensor states in Google Assistant (#127652)
This commit is contained in:
parent
784ad20fb6
commit
f7f1830b7e
@ -78,6 +78,7 @@ TYPE_AWNING = f"{PREFIX_TYPES}AWNING"
|
|||||||
TYPE_BLINDS = f"{PREFIX_TYPES}BLINDS"
|
TYPE_BLINDS = f"{PREFIX_TYPES}BLINDS"
|
||||||
TYPE_CAMERA = f"{PREFIX_TYPES}CAMERA"
|
TYPE_CAMERA = f"{PREFIX_TYPES}CAMERA"
|
||||||
TYPE_CURTAIN = f"{PREFIX_TYPES}CURTAIN"
|
TYPE_CURTAIN = f"{PREFIX_TYPES}CURTAIN"
|
||||||
|
TYPE_CARBON_MONOXIDE_DETECTOR = f"{PREFIX_TYPES}CARBON_MONOXIDE_DETECTOR"
|
||||||
TYPE_DEHUMIDIFIER = f"{PREFIX_TYPES}DEHUMIDIFIER"
|
TYPE_DEHUMIDIFIER = f"{PREFIX_TYPES}DEHUMIDIFIER"
|
||||||
TYPE_DOOR = f"{PREFIX_TYPES}DOOR"
|
TYPE_DOOR = f"{PREFIX_TYPES}DOOR"
|
||||||
TYPE_DOORBELL = f"{PREFIX_TYPES}DOORBELL"
|
TYPE_DOORBELL = f"{PREFIX_TYPES}DOORBELL"
|
||||||
@ -93,6 +94,7 @@ TYPE_SCENE = f"{PREFIX_TYPES}SCENE"
|
|||||||
TYPE_SENSOR = f"{PREFIX_TYPES}SENSOR"
|
TYPE_SENSOR = f"{PREFIX_TYPES}SENSOR"
|
||||||
TYPE_SETTOP = f"{PREFIX_TYPES}SETTOP"
|
TYPE_SETTOP = f"{PREFIX_TYPES}SETTOP"
|
||||||
TYPE_SHUTTER = f"{PREFIX_TYPES}SHUTTER"
|
TYPE_SHUTTER = f"{PREFIX_TYPES}SHUTTER"
|
||||||
|
TYPE_SMOKE_DETECTOR = f"{PREFIX_TYPES}SMOKE_DETECTOR"
|
||||||
TYPE_SPEAKER = f"{PREFIX_TYPES}SPEAKER"
|
TYPE_SPEAKER = f"{PREFIX_TYPES}SPEAKER"
|
||||||
TYPE_SWITCH = f"{PREFIX_TYPES}SWITCH"
|
TYPE_SWITCH = f"{PREFIX_TYPES}SWITCH"
|
||||||
TYPE_THERMOSTAT = f"{PREFIX_TYPES}THERMOSTAT"
|
TYPE_THERMOSTAT = f"{PREFIX_TYPES}THERMOSTAT"
|
||||||
@ -136,6 +138,7 @@ EVENT_SYNC_RECEIVED = "google_assistant_sync"
|
|||||||
|
|
||||||
DOMAIN_TO_GOOGLE_TYPES = {
|
DOMAIN_TO_GOOGLE_TYPES = {
|
||||||
alarm_control_panel.DOMAIN: TYPE_ALARM,
|
alarm_control_panel.DOMAIN: TYPE_ALARM,
|
||||||
|
binary_sensor.DOMAIN: TYPE_SENSOR,
|
||||||
button.DOMAIN: TYPE_SCENE,
|
button.DOMAIN: TYPE_SCENE,
|
||||||
camera.DOMAIN: TYPE_CAMERA,
|
camera.DOMAIN: TYPE_CAMERA,
|
||||||
climate.DOMAIN: TYPE_THERMOSTAT,
|
climate.DOMAIN: TYPE_THERMOSTAT,
|
||||||
@ -168,6 +171,14 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = {
|
|||||||
binary_sensor.DOMAIN,
|
binary_sensor.DOMAIN,
|
||||||
binary_sensor.BinarySensorDeviceClass.GARAGE_DOOR,
|
binary_sensor.BinarySensorDeviceClass.GARAGE_DOOR,
|
||||||
): TYPE_GARAGE,
|
): 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.AWNING): TYPE_AWNING,
|
||||||
(cover.DOMAIN, cover.CoverDeviceClass.CURTAIN): TYPE_CURTAIN,
|
(cover.DOMAIN, cover.CoverDeviceClass.CURTAIN): TYPE_CURTAIN,
|
||||||
(cover.DOMAIN, cover.CoverDeviceClass.DOOR): TYPE_DOOR,
|
(cover.DOMAIN, cover.CoverDeviceClass.DOOR): TYPE_DOOR,
|
||||||
|
@ -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
|
name = TRAIT_SENSOR_STATE
|
||||||
commands: list[str] = []
|
commands: list[str] = []
|
||||||
|
|
||||||
@ -2728,24 +2743,37 @@ class SensorStateTrait(_Trait):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def supported(cls, domain, features, device_class, _):
|
def supported(cls, domain, features, device_class, _):
|
||||||
"""Test if state is supported."""
|
"""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]:
|
def sync_attributes(self) -> dict[str, Any]:
|
||||||
"""Return attributes for a sync request."""
|
"""Return attributes for a sync request."""
|
||||||
device_class = self.state.attributes.get(ATTR_DEVICE_CLASS)
|
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(
|
||||||
return {}
|
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 = {
|
if self.state.domain == sensor.DOMAIN:
|
||||||
"name": data[0],
|
sensor_data = self.sensor_types.get(device_class)
|
||||||
"numericCapabilities": {"rawValueUnit": data[1]},
|
if device_class is None or sensor_data is None:
|
||||||
}
|
return {}
|
||||||
|
available_states: list[str] | None = None
|
||||||
if device_class == sensor.SensorDeviceClass.AQI:
|
if device_class == sensor.SensorDeviceClass.AQI:
|
||||||
sensor_state["descriptiveCapabilities"] = {
|
available_states = [
|
||||||
"availableStates": [
|
|
||||||
"healthy",
|
"healthy",
|
||||||
"moderate",
|
"moderate",
|
||||||
"unhealthy for sensitive groups",
|
"unhealthy for sensitive groups",
|
||||||
@ -2753,30 +2781,53 @@ class SensorStateTrait(_Trait):
|
|||||||
"very unhealthy",
|
"very unhealthy",
|
||||||
"hazardous",
|
"hazardous",
|
||||||
"unknown",
|
"unknown",
|
||||||
],
|
]
|
||||||
}
|
return create_sensor_state(sensor_data[0], sensor_data[1], available_states)
|
||||||
|
binary_sensor_data = self.binary_sensor_types.get(device_class)
|
||||||
return {"sensorStatesSupported": [sensor_state]}
|
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]:
|
def query_attributes(self) -> dict[str, Any]:
|
||||||
"""Return the attributes of this trait for this entity."""
|
"""Return the attributes of this trait for this entity."""
|
||||||
device_class = self.state.attributes.get(ATTR_DEVICE_CLASS)
|
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 {}
|
return {}
|
||||||
|
value = {
|
||||||
try:
|
STATE_ON: 0,
|
||||||
value = float(self.state.state)
|
STATE_OFF: 1,
|
||||||
except ValueError:
|
STATE_UNKNOWN: 2,
|
||||||
value = None
|
}[self.state.state]
|
||||||
if self.state.state == STATE_UNKNOWN:
|
return create_sensor_state(
|
||||||
value = None
|
binary_sensor_data[0], current_state=binary_sensor_data[1][value]
|
||||||
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]}
|
|
||||||
|
@ -4069,3 +4069,90 @@ async def test_sensorstate(
|
|||||||
)
|
)
|
||||||
is False
|
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
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user