Add binary_sensors and it's ok

This commit is contained in:
Jean-Marc Collin
2023-02-26 23:34:37 +01:00
parent e63213d22a
commit 330c3323d1
6 changed files with 311 additions and 12 deletions

View File

@@ -15,7 +15,7 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.CLIMATE]
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.BINARY_SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -58,13 +58,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return unload_ok
class VersatileThermostatAPI(Dict):
class VersatileThermostatAPI(dict):
"""The VersatileThermostatAPI"""
_hass: HomeAssistant
# _entries: Dict(str, ConfigEntry)
def __init__(self, hass):
def __init__(self, hass: HomeAssistant) -> None:
_LOGGER.debug("building a VersatileThermostatAPI")
super().__init__()
self._hass = hass
@@ -96,12 +96,11 @@ class VersatileThermostatAPI(Dict):
# Example migration function
async def async_migrate_entry(hass, config_entry: ConfigEntry):
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s", config_entry.version)
if config_entry.version == 1:
new = {**config_entry.data}
# TODO: modify Config Entry data

View File

@@ -0,0 +1,174 @@
""" Implements the VersatileThermostat binary sensors component """
import logging
from homeassistant.core import HomeAssistant, callback, Event
from homeassistant.const import STATE_ON
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .commons import VersatileThermostatBaseEntity
from .const import CONF_NAME
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the VersatileThermostat binary sensors with config flow."""
_LOGGER.debug(
"Calling async_setup_entry entry=%s, data=%s", entry.entry_id, entry.data
)
unique_id = entry.entry_id
name = entry.data.get(CONF_NAME)
async_add_entities(
[
SecurityBinarySensor(hass, unique_id, name, entry.data),
OverpoweringBinarySensor(hass, unique_id, name, entry.data),
WindowBinarySensor(hass, unique_id, name, entry.data),
MotionBinarySensor(hass, unique_id, name, entry.data),
PresenceBinarySensor(hass, unique_id, name, entry.data),
],
True,
)
class SecurityBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the security state"""
_security_state: bool
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the SecurityState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Security state"
self._attr_unique_id = f"{self._device_name}_security_state"
@callback
async def async_my_climate_changed(self, event: Event):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", event.origin.name)
old_state = self._attr_is_on
self._attr_is_on = self.my_climate.security_state
if old_state != self._attr_is_on:
self.async_write_ha_state()
return
@property
def icon(self) -> str | None:
if self._attr_is_on:
return "mdi:shield-alert"
else:
return "mdi:shield-check-outline"
class OverpoweringBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the overpowering state"""
_security_state: bool
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the OverpoweringState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Overpowering state"
self._attr_unique_id = f"{self._device_name}_overpowering_state"
@callback
async def async_my_climate_changed(self, event: Event):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", event.origin.name)
old_state = self._attr_is_on
self._attr_is_on = self.my_climate.overpowering_state
if old_state != self._attr_is_on:
self.async_write_ha_state()
return
@property
def icon(self) -> str | None:
return "mdi:flash-alert"
class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the window state"""
_security_state: bool
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the WindowState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Window state"
self._attr_unique_id = f"{self._device_name}_window_state"
@callback
async def async_my_climate_changed(self, event: Event):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", event.origin.name)
old_state = self._attr_is_on
self._attr_is_on = self.my_climate.window_state == STATE_ON
if old_state != self._attr_is_on:
self.async_write_ha_state()
return
@property
def icon(self) -> str | None:
return "mdi:window-open-variant"
class MotionBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the motion state"""
_security_state: bool
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the MotionState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Motion state"
self._attr_unique_id = f"{self._device_name}_motion_state"
@callback
async def async_my_climate_changed(self, event: Event):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", event.origin.name)
old_state = self._attr_is_on
self._attr_is_on = self.my_climate.motion_state == STATE_ON
if old_state != self._attr_is_on:
self.async_write_ha_state()
return
@property
def icon(self) -> str | None:
return "mdi:motion-sensor"
class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor which exposes the presence state"""
_security_state: bool
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the PresenceState Binary sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Presence state"
self._attr_unique_id = f"{self._device_name}_presence_state"
@callback
async def async_my_climate_changed(self, event: Event):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", event.origin.name)
old_state = self._attr_is_on
self._attr_is_on = self.my_climate.presence_state == STATE_ON
if old_state != self._attr_is_on:
self.async_write_ha_state()
return
@property
def icon(self) -> str | None:
return "mdi:home-account"

View File

@@ -21,6 +21,7 @@ from homeassistant.core import (
from homeassistant.components.climate import ClimateEntity
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity import DeviceInfo, DeviceEntryType
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_component import EntityComponent
import homeassistant.helpers.config_validation as cv
@@ -91,7 +92,8 @@ from homeassistant.const import (
)
from .const import (
# DOMAIN,
DOMAIN,
DEVICE_MANUFACTURER,
CONF_HEATER,
CONF_POWER_SENSOR,
CONF_TEMP_SENSOR,
@@ -268,6 +270,17 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self.post_init(entry_infos)
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, self._unique_id)},
name=self._name,
manufacturer=DEVICE_MANUFACTURER,
model=DOMAIN,
)
def post_init(self, entry_infos):
"""Finish the initialization of the thermostast"""
@@ -1022,11 +1035,21 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"""Get the window_state"""
return self._window_state
@property
def security_state(self) -> bool | None:
"""Get the security_state"""
return self._security_state
@property
def motion_state(self) -> bool | None:
"""Get the motion_state"""
return self._motion_state
@property
def presence_state(self) -> bool | None:
"""Get the presence_state"""
return self._presence_state
def turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on."""
if self._is_over_climate and self._underlying_climate:
@@ -1467,7 +1490,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
async def _async_climate_changed(self, event):
"""Handle unerdlying climate state changes."""
new_state = event.data.get("new_state")
_LOGGER.warning("%s - _async_climate_changed new_state is %s", self, new_state)
_LOGGER.debug("%s - _async_climate_changed new_state is %s", self, new_state)
old_state = event.data.get("old_state")
old_hvac_action = (
old_state.attributes.get("hvac_action")

View File

@@ -0,0 +1,100 @@
""" Some usefull commons class """
import logging
from datetime import timedelta
from homeassistant.core import HomeAssistant, callback, Event
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity, DeviceInfo, DeviceEntryType
from homeassistant.helpers.event import async_track_state_change_event, async_call_later
from .climate import VersatileThermostat
from .const import DOMAIN, DEVICE_MANUFACTURER
_LOGGER = logging.getLogger(__name__)
class VersatileThermostatBaseEntity(Entity):
"""A base class for all entities"""
_my_climate: VersatileThermostat
hass: HomeAssistant
_config_id: str
_devince_name: str
def __init__(self, hass: HomeAssistant, config_id, device_name) -> None:
"""The CTOR"""
self.hass = hass
self._config_id = config_id
self._device_name = device_name
self._my_climate = None
self._cancel_call = None
self._attr_has_entity_name = True
@property
def should_poll(self) -> bool:
"""Do not poll for those entities"""
return False
@property
def my_climate(self) -> VersatileThermostat | None:
"""Returns my climate if found"""
if not self._my_climate:
self._my_climate = self.find_my_versatile_thermostat()
return self._my_climate
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, self._config_id)},
name=self._device_name,
manufacturer=DEVICE_MANUFACTURER,
model=DOMAIN,
)
def find_my_versatile_thermostat(self) -> VersatileThermostat:
"""Find the underlying climate entity"""
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
_LOGGER.debug("Device_info is %s", entity.device_info)
if entity.device_info == self.device_info:
_LOGGER.debug("Found %s!", entity)
return entity
return None
@callback
async def async_added_to_hass(self):
"""Listen to my climate state change"""
# Check delay condition
async def try_find_climate(_):
_LOGGER.debug(
"%s - Calling VersatileThermostatBaseEntity.async_added_to_hass", self
)
mcl = self.my_climate
if mcl:
if self._cancel_call:
self._cancel_call()
self._cancel_call = None
self.async_on_remove(
async_track_state_change_event(
self.hass,
[mcl.entity_id],
self.async_my_climate_changed,
)
)
else:
_LOGGER.warning("%s - no entity to listen. Try later", self)
self._cancel_call = async_call_later(
self.hass, timedelta(seconds=1), try_find_climate
)
await try_find_climate(None)
@callback
async def async_my_climate_changed(self, event: Event):
"""Called when my climate have change
This method aims to be overriden to take the status change
"""
return

View File

@@ -2,16 +2,19 @@
from enum import Enum
from homeassistant.const import CONF_NAME
from homeassistant.components.climate.const import (
from homeassistant.components.climate import (
# PRESET_ACTIVITY,
PRESET_BOOST,
PRESET_COMFORT,
PRESET_ECO,
SUPPORT_TARGET_TEMPERATURE,
ClimateEntityFeature,
)
from homeassistant.exceptions import HomeAssistantError
DEVICE_MANUFACTURER = "JMCOLLIN"
DEVICE_MODEL = "Versatile Thermostat"
from .prop_algorithm import (
PROPORTIONAL_FUNCTION_TPI,
)
@@ -126,7 +129,7 @@ CONF_FUNCTIONS = [
CONF_THERMOSTAT_TYPES = [CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_CLIMATE]
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE
SERVICE_SET_PRESENCE = "set_presence"
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"

View File

@@ -14,6 +14,6 @@
"quality_scale": "silver",
"requirements": [],
"ssdp": [],
"version": "0.0.1",
"version": "3.0.0",
"zeroconf": []
}
}