Add Sensibo Climate React (#78221)

This commit is contained in:
G Johansson 2022-10-23 22:22:14 +02:00 committed by GitHub
parent 746bdb44ac
commit b04165b495
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 623 additions and 2 deletions

View File

@ -38,6 +38,12 @@ ATTR_MINUTES = "minutes"
SERVICE_ENABLE_PURE_BOOST = "enable_pure_boost"
SERVICE_DISABLE_PURE_BOOST = "disable_pure_boost"
SERVICE_FULL_STATE = "full_state"
SERVICE_ENABLE_CLIMATE_REACT = "enable_climate_react"
ATTR_HIGH_TEMPERATURE_THRESHOLD = "high_temperature_threshold"
ATTR_HIGH_TEMPERATURE_STATE = "high_temperature_state"
ATTR_LOW_TEMPERATURE_THRESHOLD = "low_temperature_threshold"
ATTR_LOW_TEMPERATURE_STATE = "low_temperature_state"
ATTR_SMART_TYPE = "smart_type"
ATTR_AC_INTEGRATION = "ac_integration"
ATTR_GEO_INTEGRATION = "geo_integration"
@ -140,6 +146,20 @@ async def async_setup_entry(
"async_full_ac_state",
)
platform.async_register_entity_service(
SERVICE_ENABLE_CLIMATE_REACT,
{
vol.Required(ATTR_HIGH_TEMPERATURE_THRESHOLD): float,
vol.Required(ATTR_HIGH_TEMPERATURE_STATE): dict,
vol.Required(ATTR_LOW_TEMPERATURE_THRESHOLD): float,
vol.Required(ATTR_LOW_TEMPERATURE_STATE): dict,
vol.Required(ATTR_SMART_TYPE): vol.In(
["temperature", "feelsLike", "humidity"]
),
},
"async_enable_climate_react",
)
class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
"""Representation of a Sensibo device."""
@ -430,6 +450,42 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
data=params,
)
async def async_enable_climate_react(
self,
high_temperature_threshold: float,
high_temperature_state: dict[str, Any],
low_temperature_threshold: float,
low_temperature_state: dict[str, Any],
smart_type: str,
) -> None:
"""Enable Climate React Configuration."""
high_temp = high_temperature_threshold
low_temp = low_temperature_threshold
if high_temperature_state.get("temperatureUnit") == "F":
high_temp = TemperatureConverter.convert(
high_temperature_threshold, TEMP_FAHRENHEIT, TEMP_CELSIUS
)
low_temp = TemperatureConverter.convert(
low_temperature_threshold, TEMP_FAHRENHEIT, TEMP_CELSIUS
)
params: dict[str, str | bool | float | dict] = {
"enabled": True,
"deviceUid": self._device_id,
"highTemperatureState": high_temperature_state,
"highTemperatureThreshold": high_temp,
"lowTemperatureState": low_temperature_state,
"lowTemperatureThreshold": low_temp,
"type": smart_type,
}
await self.api_call_custom_service_climate_react(
key="smart_on",
value=True,
data=params,
)
@async_handle_api_call
async def async_send_api_call(
self,
@ -470,6 +526,17 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
result = await self._client.async_set_pureboost(self._device_id, data)
return bool(result.get("status") == "success")
@async_handle_api_call
async def api_call_custom_service_climate_react(
self,
key: str,
value: Any,
data: dict,
) -> bool:
"""Make service call to api."""
result = await self._client.async_set_climate_react(self._device_id, data)
return bool(result.get("status") == "success")
@async_handle_api_call
async def api_call_custom_service_full_ac_state(
self,

View File

@ -155,6 +155,32 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = (
extra_fn=None,
entity_registry_enabled_default=False,
),
SensiboDeviceSensorEntityDescription(
key="climate_react_low",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
name="Climate React low temperature threshold",
value_fn=lambda data: data.smart_low_temp_threshold,
extra_fn=lambda data: data.smart_low_state,
entity_registry_enabled_default=False,
),
SensiboDeviceSensorEntityDescription(
key="climate_react_high",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
name="Climate React high temperature threshold",
value_fn=lambda data: data.smart_high_temp_threshold,
extra_fn=lambda data: data.smart_high_state,
entity_registry_enabled_default=False,
),
SensiboDeviceSensorEntityDescription(
key="climate_react_type",
device_class="sensibo__smart_type",
name="Climate React type",
value_fn=lambda data: data.smart_type,
extra_fn=None,
entity_registry_enabled_default=False,
),
FILTER_LAST_RESET_DESCRIPTION,
)
@ -281,7 +307,10 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, SensorEntity):
@property
def native_value(self) -> StateType | datetime:
"""Return value of sensor."""
return self.entity_description.value_fn(self.device_data)
state = self.entity_description.value_fn(self.device_data)
if isinstance(state, str):
return state.lower()
return state
@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:

View File

@ -146,3 +146,56 @@ full_state:
options:
- "on"
- "off"
enable_climate_react:
name: Enable Climate React
description: Enable and configure Climate React
target:
entity:
integration: sensibo
domain: climate
fields:
high_temperature_threshold:
name: Threshold high
description: When temp/humidity goes above
required: true
example: 24
selector:
number:
min: 0
max: 150
step: 0.1
mode: box
high_temperature_state:
name: State high threshold
description: What should happen at high threshold. Requires full state
required: true
selector:
object:
low_temperature_threshold:
name: Threshold low
description: When temp/humidity goes below
required: true
example: 19
selector:
number:
min: 0
max: 150
step: 0.1
mode: box
low_temperature_state:
name: State low threshold
description: What should happen at low threshold. Requires full state
required: true
selector:
object:
smart_type:
name: Trigger type
description: Choose between temperature/feels like/humidity
required: true
example: "temperature"
selector:
select:
options:
- "temperature"
- "feelsLike"
- "humidity"

View File

@ -3,6 +3,11 @@
"sensibo__sensitivity": {
"n": "Normal",
"s": "Sensitive"
},
"sensibo__smart_type": {
"temperature": "Temperature",
"feelslike": "Feels like",
"humidity": "Humidity"
}
}
}

View File

@ -14,6 +14,7 @@ from homeassistant.components.switch import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
@ -53,6 +54,17 @@ DEVICE_SWITCH_TYPES: tuple[SensiboDeviceSwitchEntityDescription, ...] = (
command_off="async_turn_off_timer",
data_key="timer_on",
),
SensiboDeviceSwitchEntityDescription(
key="climate_react_switch",
device_class=SwitchDeviceClass.SWITCH,
name="Climate React",
icon="mdi:wizard-hat",
value_fn=lambda data: data.smart_on,
extra_fn=lambda data: {"type": data.smart_type},
command_on="async_turn_on_off_smart",
command_off="async_turn_on_off_smart",
data_key="smart_on",
),
)
PURE_SWITCH_TYPES: tuple[SensiboDeviceSwitchEntityDescription, ...] = (
@ -166,3 +178,15 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity):
data["primeIntegration"] = False
result = await self._client.async_set_pureboost(self._device_id, data)
return bool(result.get("status") == "success")
@async_handle_api_call
async def async_turn_on_off_smart(self, key: str, value: Any) -> bool:
"""Make service call to api for setting Climate React."""
if self.device_data.smart_type is None:
raise HomeAssistantError(
"Use Sensibo Enable Climate React Service once to enable switch or the Sensibo app"
)
new_state = bool(self.device_data.smart_on is False)
data: dict[str, Any] = {"enabled": new_state}
result = await self._client.async_enable_climate_react(self._device_id, data)
return bool(result.get("status") == "success")

View File

@ -3,6 +3,11 @@
"sensibo__sensitivity": {
"n": "Normal",
"s": "Sensitive"
},
"sensibo__smart_type": {
"feelslike": "Feels like",
"humidity": "Humidity",
"temperature": "Temperature"
}
}
}

View File

@ -23,14 +23,20 @@ from homeassistant.components.climate import (
from homeassistant.components.sensibo.climate import (
ATTR_AC_INTEGRATION,
ATTR_GEO_INTEGRATION,
ATTR_HIGH_TEMPERATURE_STATE,
ATTR_HIGH_TEMPERATURE_THRESHOLD,
ATTR_HORIZONTAL_SWING_MODE,
ATTR_INDOOR_INTEGRATION,
ATTR_LIGHT,
ATTR_LOW_TEMPERATURE_STATE,
ATTR_LOW_TEMPERATURE_THRESHOLD,
ATTR_MINUTES,
ATTR_OUTDOOR_INTEGRATION,
ATTR_SENSITIVITY,
ATTR_SMART_TYPE,
ATTR_TARGET_TEMPERATURE,
SERVICE_ASSUME_STATE,
SERVICE_ENABLE_CLIMATE_REACT,
SERVICE_ENABLE_PURE_BOOST,
SERVICE_ENABLE_TIMER,
SERVICE_FULL_STATE,
@ -923,6 +929,311 @@ async def test_climate_pure_boost(
assert state4.state == "s"
async def test_climate_climate_react(
hass: HomeAssistant,
entity_registry_enabled_by_default: AsyncMock,
load_int: ConfigEntry,
monkeypatch: pytest.MonkeyPatch,
get_data: SensiboData,
) -> None:
"""Test the Sensibo climate react custom service."""
with patch(
"homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
return_value=get_data,
):
async_fire_time_changed(
hass,
dt.utcnow() + timedelta(minutes=5),
)
await hass.async_block_till_done()
state_climate = hass.states.get("climate.hallway")
with patch(
"homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data",
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_set_climate_react",
):
with pytest.raises(MultipleInvalid):
await hass.services.async_call(
DOMAIN,
SERVICE_ENABLE_PURE_BOOST,
{
ATTR_ENTITY_ID: state_climate.entity_id,
ATTR_LOW_TEMPERATURE_THRESHOLD: 0.2,
ATTR_HIGH_TEMPERATURE_THRESHOLD: 30.3,
ATTR_SMART_TYPE: "temperature",
},
blocking=True,
)
await hass.async_block_till_done()
with patch(
"homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data",
return_value=get_data,
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_set_climate_react",
return_value={
"status": "success",
"result": {
"enabled": True,
"deviceUid": "ABC999111",
"highTemperatureState": {
"on": True,
"targetTemperature": 15,
"temperatureUnit": "C",
"mode": "cool",
"fanLevel": "high",
"swing": "stopped",
"horizontalSwing": "stopped",
"light": "on",
},
"highTemperatureThreshold": 30.5,
"lowTemperatureState": {
"on": True,
"targetTemperature": 25,
"temperatureUnit": "C",
"mode": "heat",
"fanLevel": "low",
"swing": "stopped",
"horizontalSwing": "stopped",
"light": "on",
},
"lowTemperatureThreshold": 5.5,
"type": "temperature",
},
},
):
await hass.services.async_call(
DOMAIN,
SERVICE_ENABLE_CLIMATE_REACT,
{
ATTR_ENTITY_ID: state_climate.entity_id,
ATTR_LOW_TEMPERATURE_THRESHOLD: 5.5,
ATTR_HIGH_TEMPERATURE_THRESHOLD: 30.5,
ATTR_LOW_TEMPERATURE_STATE: {
"on": True,
"targetTemperature": 25,
"temperatureUnit": "C",
"mode": "heat",
"fanLevel": "low",
"swing": "stopped",
"horizontalSwing": "stopped",
"light": "on",
},
ATTR_HIGH_TEMPERATURE_STATE: {
"on": True,
"targetTemperature": 15,
"temperatureUnit": "C",
"mode": "cool",
"fanLevel": "high",
"swing": "stopped",
"horizontalSwing": "stopped",
"light": "on",
},
ATTR_SMART_TYPE: "temperature",
},
blocking=True,
)
await hass.async_block_till_done()
monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_on", True)
monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_type", "temperature")
monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_low_temp_threshold", 5.5)
monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_high_temp_threshold", 30.5)
monkeypatch.setattr(
get_data.parsed["ABC999111"],
"smart_low_state",
{
"on": True,
"targetTemperature": 25,
"temperatureUnit": "C",
"mode": "heat",
"fanLevel": "low",
"swing": "stopped",
"horizontalSwing": "stopped",
"light": "on",
},
)
monkeypatch.setattr(
get_data.parsed["ABC999111"],
"smart_high_state",
{
"on": True,
"targetTemperature": 15,
"temperatureUnit": "C",
"mode": "cool",
"fanLevel": "high",
"swing": "stopped",
"horizontalSwing": "stopped",
"light": "on",
},
)
with patch(
"homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
return_value=get_data,
):
async_fire_time_changed(
hass,
dt.utcnow() + timedelta(minutes=5),
)
await hass.async_block_till_done()
state1 = hass.states.get("switch.hallway_climate_react")
state2 = hass.states.get("sensor.hallway_climate_react_low_temperature_threshold")
state3 = hass.states.get("sensor.hallway_climate_react_high_temperature_threshold")
state4 = hass.states.get("sensor.hallway_climate_react_type")
assert state1.state == "on"
assert state2.state == "5.5"
assert state3.state == "30.5"
assert state4.state == "temperature"
async def test_climate_climate_react_fahrenheit(
hass: HomeAssistant,
entity_registry_enabled_by_default: AsyncMock,
load_int: ConfigEntry,
monkeypatch: pytest.MonkeyPatch,
get_data: SensiboData,
) -> None:
"""Test the Sensibo climate react custom service with fahrenheit."""
with patch(
"homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
return_value=get_data,
):
async_fire_time_changed(
hass,
dt.utcnow() + timedelta(minutes=5),
)
await hass.async_block_till_done()
state_climate = hass.states.get("climate.hallway")
with patch(
"homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data",
return_value=get_data,
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_set_climate_react",
return_value={
"status": "success",
"result": {
"enabled": True,
"deviceUid": "ABC999111",
"highTemperatureState": {
"on": True,
"targetTemperature": 65,
"temperatureUnit": "F",
"mode": "cool",
"fanLevel": "high",
"swing": "stopped",
"horizontalSwing": "stopped",
"light": "on",
},
"highTemperatureThreshold": 77,
"lowTemperatureState": {
"on": True,
"targetTemperature": 85,
"temperatureUnit": "F",
"mode": "heat",
"fanLevel": "low",
"swing": "stopped",
"horizontalSwing": "stopped",
"light": "on",
},
"lowTemperatureThreshold": 32,
"type": "temperature",
},
},
):
await hass.services.async_call(
DOMAIN,
SERVICE_ENABLE_CLIMATE_REACT,
{
ATTR_ENTITY_ID: state_climate.entity_id,
ATTR_LOW_TEMPERATURE_THRESHOLD: 32.0,
ATTR_HIGH_TEMPERATURE_THRESHOLD: 77.0,
ATTR_LOW_TEMPERATURE_STATE: {
"on": True,
"targetTemperature": 85,
"temperatureUnit": "F",
"mode": "heat",
"fanLevel": "low",
"swing": "stopped",
"horizontalSwing": "stopped",
"light": "on",
},
ATTR_HIGH_TEMPERATURE_STATE: {
"on": True,
"targetTemperature": 65,
"temperatureUnit": "F",
"mode": "cool",
"fanLevel": "high",
"swing": "stopped",
"horizontalSwing": "stopped",
"light": "on",
},
ATTR_SMART_TYPE: "temperature",
},
blocking=True,
)
await hass.async_block_till_done()
monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_on", True)
monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_type", "temperature")
monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_low_temp_threshold", 0)
monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_high_temp_threshold", 25)
monkeypatch.setattr(
get_data.parsed["ABC999111"],
"smart_low_state",
{
"on": True,
"targetTemperature": 85,
"temperatureUnit": "F",
"mode": "heat",
"fanLevel": "low",
"swing": "stopped",
"horizontalSwing": "stopped",
"light": "on",
},
)
monkeypatch.setattr(
get_data.parsed["ABC999111"],
"smart_high_state",
{
"on": True,
"targetTemperature": 65,
"temperatureUnit": "F",
"mode": "cool",
"fanLevel": "high",
"swing": "stopped",
"horizontalSwing": "stopped",
"light": "on",
},
)
with patch(
"homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
return_value=get_data,
):
async_fire_time_changed(
hass,
dt.utcnow() + timedelta(minutes=5),
)
await hass.async_block_till_done()
state1 = hass.states.get("switch.hallway_climate_react")
state2 = hass.states.get("sensor.hallway_climate_react_low_temperature_threshold")
state3 = hass.states.get("sensor.hallway_climate_react_high_temperature_threshold")
state4 = hass.states.get("sensor.hallway_climate_react_type")
assert state1.state == "on"
assert state2.state == "0"
assert state3.state == "25"
assert state4.state == "temperature"
async def test_climate_full_ac_state(
hass: HomeAssistant,
entity_registry_enabled_by_default: AsyncMock,

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import timedelta
from unittest.mock import patch
from unittest.mock import AsyncMock, patch
from pysensibo.model import SensiboData
from pytest import MonkeyPatch
@ -16,6 +16,7 @@ from tests.common import async_fire_time_changed
async def test_sensor(
hass: HomeAssistant,
entity_registry_enabled_by_default: AsyncMock,
load_int: ConfigEntry,
monkeypatch: MonkeyPatch,
get_data: SensiboData,
@ -25,9 +26,11 @@ async def test_sensor(
state1 = hass.states.get("sensor.hallway_motion_sensor_battery_voltage")
state2 = hass.states.get("sensor.kitchen_pm2_5")
state3 = hass.states.get("sensor.kitchen_pure_sensitivity")
state4 = hass.states.get("sensor.hallway_climate_react_low_temperature_threshold")
assert state1.state == "3000"
assert state2.state == "1"
assert state3.state == "n"
assert state4.state == "0.0"
assert state2.attributes == {
"state_class": "measurement",
"unit_of_measurement": "µg/m³",
@ -35,6 +38,20 @@ async def test_sensor(
"icon": "mdi:air-filter",
"friendly_name": "Kitchen PM2.5",
}
assert state4.attributes == {
"device_class": "temperature",
"friendly_name": "Hallway Climate React low temperature threshold",
"state_class": "measurement",
"unit_of_measurement": "°C",
"on": True,
"targetTemperature": 21,
"temperatureUnit": "C",
"mode": "heat",
"fanLevel": "low",
"swing": "stopped",
"horizontalSwing": "stopped",
"light": "on",
}
monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pm25", 2)

View File

@ -223,3 +223,113 @@ async def test_switch_command_failure(
},
blocking=True,
)
async def test_switch_climate_react(
hass: HomeAssistant,
load_int: ConfigEntry,
monkeypatch: MonkeyPatch,
get_data: SensiboData,
) -> None:
"""Test the Sensibo switch for climate react."""
state1 = hass.states.get("switch.hallway_climate_react")
assert state1.state == STATE_OFF
with patch(
"homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data",
return_value=get_data,
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_enable_climate_react",
return_value={"status": "success"},
):
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: state1.entity_id,
},
blocking=True,
)
await hass.async_block_till_done()
monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_on", True)
with patch(
"homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
return_value=get_data,
):
async_fire_time_changed(
hass,
dt.utcnow() + timedelta(minutes=5),
)
await hass.async_block_till_done()
state1 = hass.states.get("switch.hallway_climate_react")
assert state1.state == STATE_ON
with patch(
"homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data",
return_value=get_data,
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_enable_climate_react",
return_value={"status": "success"},
):
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{
ATTR_ENTITY_ID: state1.entity_id,
},
blocking=True,
)
await hass.async_block_till_done()
monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_on", False)
with patch(
"homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
return_value=get_data,
):
async_fire_time_changed(
hass,
dt.utcnow() + timedelta(minutes=5),
)
await hass.async_block_till_done()
state1 = hass.states.get("switch.hallway_climate_react")
assert state1.state == STATE_OFF
async def test_switch_climate_react_no_data(
hass: HomeAssistant,
load_int: ConfigEntry,
monkeypatch: MonkeyPatch,
get_data: SensiboData,
) -> None:
"""Test the Sensibo switch for climate react."""
monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_type", None)
with patch(
"homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
return_value=get_data,
):
async_fire_time_changed(
hass,
dt.utcnow() + timedelta(minutes=5),
)
await hass.async_block_till_done()
state1 = hass.states.get("switch.hallway_climate_react")
assert state1.state == STATE_OFF
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: state1.entity_id,
},
blocking=True,
)
await hass.async_block_till_done()