Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,10 @@ omit =
homeassistant/components/linode/*
homeassistant/components/linux_battery/sensor.py
homeassistant/components/lirc/*
homeassistant/components/livisi/__init__.py
homeassistant/components/livisi/climate.py
Comment thread
StefanIacobLivisi marked this conversation as resolved.
homeassistant/components/livisi/coordinator.py
homeassistant/components/livisi/switch.py
homeassistant/components/llamalab_automate/notify.py
homeassistant/components/logi_circle/__init__.py
homeassistant/components/logi_circle/camera.py
Expand Down
5 changes: 3 additions & 2 deletions homeassistant/components/livisi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@

from homeassistant import core
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, device_registry as dr

from .const import DOMAIN, SWITCH_PLATFORM
from .const import DOMAIN
from .coordinator import LivisiDataUpdateCoordinator

PLATFORMS: Final = [SWITCH_PLATFORM]
PLATFORMS: Final = [Platform.CLIMATE, Platform.SWITCH]


async def async_setup_entry(hass: core.HomeAssistant, entry: ConfigEntry) -> bool:
Expand Down
215 changes: 215 additions & 0 deletions homeassistant/components/livisi/climate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
"""Code to handle a Livisi Virtual Climate Control."""
Comment thread
StefanIacobLivisi marked this conversation as resolved.
Outdated
from __future__ import annotations

from collections.abc import Mapping
from typing import Any

from aiolivisi.const import CAPABILITY_MAP

from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import (
DOMAIN,
LIVISI_REACHABILITY_CHANGE,
LIVISI_STATE_CHANGE,
LOGGER,
MAX_TEMPERATURE,
MIN_TEMPERATURE,
VRCC_DEVICE_TYPE,
)
from .coordinator import LivisiDataUpdateCoordinator


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up climate device."""
coordinator: LivisiDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]

@callback
def handle_coordinator_update() -> None:
"""Add climate device."""
shc_devices: list[dict[str, Any]] = coordinator.data
entities: list[ClimateEntity] = []
for device in shc_devices:
if (
device["type"] == VRCC_DEVICE_TYPE
and device["id"] not in coordinator.devices
):
livisi_climate: ClimateEntity = create_entity(
config_entry, device, coordinator
)
LOGGER.debug("Include device type: %s", device.get("type"))
coordinator.devices.add(device["id"])
entities.append(livisi_climate)
async_add_entities(entities)

config_entry.async_on_unload(
coordinator.async_add_listener(handle_coordinator_update)
)


def create_entity(
config_entry: ConfigEntry,
device: dict[str, Any],
coordinator: LivisiDataUpdateCoordinator,
) -> ClimateEntity:
"""Create Climate Entity."""
capabilities: Mapping[str, Any] = device[CAPABILITY_MAP]
room_id: str = device["location"]
room_name: str = coordinator.rooms[room_id]
livisi_climate = LivisiClimate(
config_entry,
coordinator,
unique_id=device["id"],
manufacturer=device["manufacturer"],
device_type=device["type"],
target_temperature_capability=capabilities["RoomSetpoint"],
temperature_capability=capabilities["RoomTemperature"],
humidity_capability=capabilities["RoomHumidity"],
room=room_name,
)
return livisi_climate


class LivisiClimate(CoordinatorEntity[LivisiDataUpdateCoordinator], ClimateEntity):
"""Represents the Livisi Climate."""

_attr_hvac_modes = [HVACMode.HEAT]
_attr_hvac_mode = HVACMode.HEAT
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
_attr_target_temperature_high = MAX_TEMPERATURE
_attr_target_temperature_low = MIN_TEMPERATURE

def __init__(
self,
config_entry: ConfigEntry,
coordinator: LivisiDataUpdateCoordinator,
unique_id: str,
manufacturer: str,
device_type: str,
target_temperature_capability: str,
temperature_capability: str,
humidity_capability: str,
room: str,
) -> None:
"""Initialize the Livisi Climate."""
self.config_entry = config_entry
self._attr_unique_id = unique_id
self._target_temperature_capability = target_temperature_capability
self._temperature_capability = temperature_capability
self._humidity_capability = humidity_capability
self.aio_livisi = coordinator.aiolivisi
self._attr_available = False
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
manufacturer=manufacturer,
model=device_type,
name=room,
suggested_area=room,
via_device=(DOMAIN, config_entry.entry_id),
)
super().__init__(coordinator)

async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
response = await self.aio_livisi.async_vrcc_set_temperature(
self._target_temperature_capability,
kwargs.get(ATTR_TEMPERATURE),
self.coordinator.is_avatar,
)
if response is None:
self._attr_available = False
raise HomeAssistantError(f"Failed to turn off {self._attr_name}")

def set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Do nothing as LIVISI devices do not support changing the hvac mode."""
Comment thread
StefanIacobLivisi marked this conversation as resolved.
raise HomeAssistantError(
"This feature is not supported with the LIVISI climate devices"
)

async def async_added_to_hass(self) -> None:
"""Register callbacks."""
target_temperature = await self.coordinator.async_get_vrcc_target_temperature(
self._target_temperature_capability
)
temperature = await self.coordinator.async_get_vrcc_temperature(
self._temperature_capability
)
humidity = await self.coordinator.async_get_vrcc_humidity(
self._humidity_capability
)
if temperature is None:
self._attr_current_temperature = None
self._attr_available = False
else:
self._attr_target_temperature = target_temperature
self._attr_current_temperature = temperature
self._attr_current_humidity = humidity
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{LIVISI_STATE_CHANGE}_{self._target_temperature_capability}",
self.update_target_temperature,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{LIVISI_STATE_CHANGE}_{self._temperature_capability}",
self.update_temperature,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{LIVISI_STATE_CHANGE}_{self._humidity_capability}",
self.update_humidity,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{LIVISI_REACHABILITY_CHANGE}_{self.unique_id}",
self.update_reachability,
)
)

@callback
def update_target_temperature(self, target_temperature: float) -> None:
"""Update the target temperature of the climate device."""
self._attr_target_temperature = target_temperature
self.async_write_ha_state()

@callback
def update_temperature(self, current_temperature: float) -> None:
"""Update the current temperature of the climate device."""
self._attr_current_temperature = current_temperature
self.async_write_ha_state()

@callback
def update_humidity(self, humidity: int) -> None:
"""Update the humidity temperature of the climate device."""
self._attr_current_humidity = humidity
self.async_write_ha_state()

@callback
def update_reachability(self, is_reachable: bool) -> None:
"""Update the reachability of the climate device."""
self._attr_available = is_reachable
self.async_write_ha_state()
7 changes: 5 additions & 2 deletions homeassistant/components/livisi/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@

CONF_HOST = "host"
CONF_PASSWORD: Final = "password"
AVATAR = "Avatar"
AVATAR_PORT: Final = 9090
CLASSIC_PORT: Final = 8080
DEVICE_POLLING_DELAY: Final = 60
LIVISI_STATE_CHANGE: Final = "livisi_state_change"
LIVISI_REACHABILITY_CHANGE: Final = "livisi_reachability_change"

SWITCH_PLATFORM: Final = "switch"

PSS_DEVICE_TYPE: Final = "PSS"
VRCC_DEVICE_TYPE: Final = "VRCC"

MAX_TEMPERATURE: Final = 30.0
MIN_TEMPERATURE: Final = 6.0
42 changes: 39 additions & 3 deletions homeassistant/components/livisi/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import (
AVATAR,
AVATAR_PORT,
CLASSIC_PORT,
CONF_HOST,
Expand Down Expand Up @@ -69,29 +70,58 @@ async def async_setup(self) -> None:
livisi_connection_data=livisi_connection_data
)
controller_data = await self.aiolivisi.async_get_controller()
if controller_data["controllerType"] == "Avatar":
if (controller_type := controller_data["controllerType"]) == AVATAR:
self.port = AVATAR_PORT
self.is_avatar = True
else:
self.port = CLASSIC_PORT
self.is_avatar = False
self.controller_type = controller_type
Comment thread
StefanIacobLivisi marked this conversation as resolved.
Outdated
self.serial_number = controller_data["serialNumber"]
self.controller_type = controller_data["controllerType"]

async def async_get_devices(self) -> list[dict[str, Any]]:
"""Set the discovered devices list."""
return await self.aiolivisi.async_get_devices()

async def async_get_pss_state(self, capability: str) -> bool | None:
"""Set the PSS state."""
response: dict[str, Any] = await self.aiolivisi.async_get_device_state(
response: dict[str, Any] | None = await self.aiolivisi.async_get_device_state(
capability[1:]
)
if response is None:
return None
on_state = response["onState"]
return on_state["value"]

async def async_get_vrcc_target_temperature(self, capability: str) -> float | None:
"""Get the target temperature of the climate device."""
response: dict[str, Any] | None = await self.aiolivisi.async_get_device_state(
capability[1:]
)
if response is None:
return None
if self.is_avatar:
return response["setpointTemperature"]["value"]
return response["pointTemperature"]["value"]

async def async_get_vrcc_temperature(self, capability: str) -> float | None:
"""Get the temperature of the climate device."""
response: dict[str, Any] | None = await self.aiolivisi.async_get_device_state(
capability[1:]
)
if response is None:
return None
return response["temperature"]["value"]

async def async_get_vrcc_humidity(self, capability: str) -> int | None:
"""Get the humidity of the climate device."""
response: dict[str, Any] | None = await self.aiolivisi.async_get_device_state(
capability[1:]
)
if response is None:
return None
return response["humidity"]["value"]

async def async_set_all_rooms(self) -> None:
"""Set the room list."""
response: list[dict[str, Any]] = await self.aiolivisi.async_get_all_rooms()
Expand All @@ -108,6 +138,12 @@ def on_data(self, event_data: LivisiEvent) -> None:
f"{LIVISI_STATE_CHANGE}_{event_data.source}",
event_data.onState,
)
if event_data.vrccData is not None:
async_dispatcher_send(
self.hass,
f"{LIVISI_STATE_CHANGE}_{event_data.source}",
event_data.vrccData,
)
if event_data.isReachable is not None:
async_dispatcher_send(
self.hass,
Expand Down