Skip to content

Commit

Permalink
Add config flow, icons, translations et turn on/off support (#15)
Browse files Browse the repository at this point in the history
* Add config flow, icons, translations et turn on/off support

* Update translations
  • Loading branch information
piitaya authored Jul 2, 2024
1 parent 4591c67 commit 221d4a7
Show file tree
Hide file tree
Showing 8 changed files with 384 additions and 52 deletions.
36 changes: 35 additions & 1 deletion custom_components/qubino_wire_pilot/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,35 @@
"""Qubino wire pilot platform configuration."""
"""Qubino wire pilot component."""

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device import (
async_remove_stale_devices_links_keep_entity_device,
)

CONF_HEATER = "heater"
DOMAIN = "qubino_wire_pilot"
PLATFORMS = [Platform.CLIMATE]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up from a config entry."""

async_remove_stale_devices_links_keep_entity_device(
hass,
entry.entry_id,
entry.options[CONF_HEATER],
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
return True


async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener, called when the config entry options are changed."""
await hass.config_entries.async_reload(entry.entry_id)


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
192 changes: 144 additions & 48 deletions custom_components/qubino_wire_pilot/climate.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""Platform for Qubino Wire Pilot."""

import logging
import math

import voluptuous as vol

from homeassistant.components.climate import (
PLATFORM_SCHEMA,
PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA,
PRESET_AWAY,
PRESET_COMFORT,
PRESET_ECO,
Expand All @@ -18,25 +20,40 @@
DOMAIN as LIGHT_DOMAIN,
SERVICE_TURN_ON as LIGHT_SERVICE_TURN_ON,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_NAME,
CONF_UNIQUE_ID,
EVENT_HOMEASSISTANT_START,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import (
CoreState,
Event,
EventStateChangedData,
HomeAssistant,
State,
callback,
)
from homeassistant.helpers import device_registry as dr, entity_registry as er
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

from . import DOMAIN, PLATFORMS

_LOGGER = logging.getLogger(__name__)

DEFAULT_NAME = "Qubino Thermostat"
DEFAULT_NAME = "Thermostat"

CONF_HEATER = "heater"
CONF_SENSOR = "sensor"
Expand All @@ -52,34 +69,68 @@
VALUE_COMFORT_1 = 50
VALUE_COMFORT = 99

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
PLATFORM_SCHEMA_COMMON = vol.Schema(
{
vol.Required(CONF_HEATER): cv.entity_id,
vol.Optional(CONF_SENSOR): cv.entity_id,
vol.Optional(CONF_ADDITIONAL_MODES, default=False): cv.boolean,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)

PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_COMMON.schema)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Initialize config entry."""
await _async_setup_config(
hass,
PLATFORM_SCHEMA_COMMON(dict(config_entry.options)),
config_entry.entry_id,
async_add_entities,
)


async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the generic thermostat platform."""

await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
await _async_setup_config(
hass, config, config.get(CONF_UNIQUE_ID), async_add_entities
)


async def _async_setup_config(
hass: HomeAssistant,
config: ConfigType,
unique_id: str | None,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the wire pilot climate platform."""
unique_id = config.get(CONF_UNIQUE_ID)
name = config.get(CONF_NAME)
heater_entity_id = config.get(CONF_HEATER)
sensor_entity_id = config.get(CONF_SENSOR)
additional_modes = config.get(CONF_ADDITIONAL_MODES)
name: str | None = config.get(CONF_NAME)
heater_entity_id: str = config.get(CONF_HEATER)
sensor_entity_id: str | None = config.get(CONF_SENSOR)
additional_modes: bool = config.get(CONF_ADDITIONAL_MODES)

async_add_entities(
[
QubinoWirePilotClimate(
unique_id, name, heater_entity_id, sensor_entity_id, additional_modes
hass,
name,
heater_entity_id,
sensor_entity_id,
additional_modes,
unique_id,
)
]
)
Expand All @@ -88,50 +139,87 @@ async def async_setup_platform(
class QubinoWirePilotClimate(ClimateEntity, RestoreEntity):
"""Representation of a Qubino Wire Pilot device."""

_attr_should_poll = False
_attr_translation_key: str = "qubino_wire_pilot"
_enable_turn_on_off_backwards_compatibility = False

def __init__(
self, unique_id, name, heater_entity_id, sensor_entity_id, additional_modes
self,
hass: HomeAssistant,
name: str | None,
heater_entity_id: str,
sensor_entity_id: str | None,
additional_modes: bool,
unique_id: str | None,
) -> None:
"""Initialize the climate device."""

registry = er.async_get(hass)
device_registry = dr.async_get(hass)
heater_entity = registry.async_get(heater_entity_id)
device_id = heater_entity.device_id if heater_entity else None
has_entity_name = heater_entity.has_entity_name if heater_entity else False

self._device_id = device_id
if device_id and (device := device_registry.async_get(device_id)):
self._attr_device_info = DeviceInfo(
connections=device.connections,
identifiers=device.identifiers,
)

if name:
self._attr_name = name

self.heater_entity_id = heater_entity_id
self.sensor_entity_id = sensor_entity_id
self.additional_modes = additional_modes
self._cur_temperature = None

self._attr_has_entity_name = has_entity_name
self._attr_unique_id = (
unique_id if unique_id else "qubino_wire_pilot_" + heater_entity_id
)
self._attr_name = name

async def async_added_to_hass(self) -> None:
"""Run when entity about to be added."""
await super().async_added_to_hass()

# Add listener
async_track_state_change(
self.hass, self.heater_entity_id, self._async_heater_changed
self.async_on_remove(
async_track_state_change_event(
self.hass, [self.sensor_entity_id], self._async_sensor_changed
)
)
if self.sensor_entity_id is not None:
async_track_state_change(
self.hass, self.sensor_entity_id, self._async_temperature_changed
self.async_on_remove(
async_track_state_change_event(
self.hass, [self.heater_entity_id], self._async_heater_changed
)
)

@callback
def _async_startup(event):
def _async_startup(_: Event | None = None) -> None:
"""Init on startup."""
if self.sensor_entity_id is not None:
sensor_state = self.hass.states.get(self.sensor_entity_id)
if sensor_state:
self._async_update_temperature(sensor_state)

self.async_schedule_update_ha_state()

self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup)
sensor_state = self.hass.states.get(self.sensor_entity_id)
if sensor_state and sensor_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._async_update_temp(sensor_state)
self.async_write_ha_state()

if self.hass.state is CoreState.running:
_async_startup()
else:
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup)

@property
def supported_features(self) -> ClimateEntityFeature:
"""Return the list of supported features."""
return ClimateEntityFeature.PRESET_MODE
return (
ClimateEntityFeature.PRESET_MODE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)

def update(self) -> None:
"""Update unit attributes."""
Expand All @@ -153,7 +241,7 @@ def heater_value(self) -> int | None:
state = self.hass.states.get(self.heater_entity_id)

if state is None:
return
return None

brightness = state.attributes.get(ATTR_BRIGHTNESS)
if brightness is None:
Expand All @@ -176,8 +264,7 @@ def preset_modes(self) -> list[str] | None:
PRESET_AWAY,
PRESET_NONE,
]
else:
return [PRESET_COMFORT, PRESET_ECO, PRESET_AWAY, PRESET_NONE]
return [PRESET_COMFORT, PRESET_ECO, PRESET_AWAY, PRESET_NONE]

@property
def preset_mode(self) -> str | None:
Expand All @@ -188,16 +275,15 @@ def preset_mode(self) -> str | None:
return None
if value <= VALUE_OFF:
return PRESET_NONE
elif value <= VALUE_FROST:
if value <= VALUE_FROST:
return PRESET_AWAY
elif value <= VALUE_ECO:
if value <= VALUE_ECO:
return PRESET_ECO
elif value <= VALUE_COMFORT_2 and self.additional_modes:
if value <= VALUE_COMFORT_2 and self.additional_modes:
return PRESET_COMFORT_2
elif value <= VALUE_COMFORT_1 and self.additional_modes:
if value <= VALUE_COMFORT_1 and self.additional_modes:
return PRESET_COMFORT_1
else:
return PRESET_COMFORT
return PRESET_COMFORT

async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set preset mode."""
Expand Down Expand Up @@ -242,28 +328,38 @@ def hvac_mode(self) -> HVACMode | None:
return None
if value <= VALUE_OFF:
return HVACMode.OFF
else:
return HVACMode.HEAT
return HVACMode.HEAT

async def _async_sensor_changed(self, event: Event[EventStateChangedData]) -> None:
"""Handle temperature changes."""
new_state = event.data["new_state"]
if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return

self._async_update_temp(new_state)
self.async_write_ha_state()

@callback
def _async_heater_changed(self, entity_id, old_state, new_state) -> None:
def _async_heater_changed(self, event: Event[EventStateChangedData]) -> None:
"""Handle heater switch state changes."""
new_state = event.data["new_state"]
if new_state is None:
return
self.async_schedule_update_ha_state()
self.async_write_ha_state()

async def _async_temperature_changed(self, entity_id, old_state, new_state) -> None:
if new_state is None:
return
self._async_update_temperature(new_state)
self._async_update_temp(new_state)
self.async_write_ha_state()

@callback
def _async_update_temperature(self, state):
def _async_update_temp(self, state: State):
try:
if state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN):
self._cur_temperature = float(state.state)
else:
self._cur_temperature = None
cur_temp = float(state.state)
if not math.isfinite(cur_temp):
raise ValueError(f"Sensor has illegal state {state.state}")
self._cur_temp = cur_temp
except ValueError as ex:
_LOGGER.error("Unable to update from temperature sensor: %s", ex)

Expand Down
Loading

0 comments on commit 221d4a7

Please sign in to comment.