Skip to content

Commit

Permalink
Rointe Integration
Browse files Browse the repository at this point in the history
  • Loading branch information
tggm committed Dec 24, 2023
1 parent 9066555 commit 20936eb
Show file tree
Hide file tree
Showing 23 changed files with 1,603 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,9 @@ omit =
homeassistant/components/ripple/sensor.py
homeassistant/components/roborock/coordinator.py
homeassistant/components/rocketchat/notify.py
homeassistant/components/rointe/__init__.py
homeassistant/components/rointe/climate.py
homeassistant/components/rointe/device_manager.py
homeassistant/components/roomba/__init__.py
homeassistant/components/roomba/binary_sensor.py
homeassistant/components/roomba/braava.py
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,8 @@ build.json @home-assistant/supervisor
/tests/components/rmvtransport/ @cgtobi
/homeassistant/components/roborock/ @humbertogontijo @Lash-L
/tests/components/roborock/ @humbertogontijo @Lash-L
/homeassistant/components/rointe/ @tggm
/tests/components/rointe/ @tggm
/homeassistant/components/roku/ @ctalkington
/tests/components/roku/ @ctalkington
/homeassistant/components/roomba/ @pschmitt @cyr-ius @shenxn @Xitee1
Expand Down
51 changes: 51 additions & 0 deletions homeassistant/components/rointe/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""The Rointe Heaters integration."""
from __future__ import annotations

from rointesdk.rointe_api import ApiResponse, RointeAPI

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady

from .const import CONF_INSTALLATION, CONF_PASSWORD, CONF_USERNAME, DOMAIN, PLATFORMS
from .coordinator import RointeDataUpdateCoordinator
from .device_manager import RointeDeviceManager


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

rointe_api = RointeAPI(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD])

# Login to the Rointe API.
login_result: ApiResponse = await hass.async_add_executor_job(
rointe_api.initialize_authentication
)

if not login_result.success:
raise ConfigEntryNotReady("Unable to connect to the Rointe API")

rointe_device_manager = RointeDeviceManager(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
installation_id=entry.data[CONF_INSTALLATION],
hass=hass,
rointe_api=rointe_api,
)

rointe_coordinator = RointeDataUpdateCoordinator(hass, rointe_device_manager)

await rointe_coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = rointe_coordinator

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry and removes event handlers."""

if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok
228 changes: 228 additions & 0 deletions homeassistant/components/rointe/climate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
"""Support for Rointe Climate."""

from __future__ import annotations

from rointesdk.device import RointeDevice

from homeassistant.components.climate import (
PRESET_COMFORT,
PRESET_ECO,
ClimateEntity,
ClimateEntityDescription,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import (
DOMAIN,
LOGGER,
RADIATOR_TEMP_MAX,
RADIATOR_TEMP_MIN,
RADIATOR_TEMP_STEP,
RointeCommand,
RointeOperationMode,
RointePreset,
)
from .coordinator import RointeDataUpdateCoordinator
from .entity import RointeRadiatorEntity

AVAILABLE_HVAC_MODES: list[HVACMode] = [HVACMode.OFF, HVACMode.HEAT, HVACMode.AUTO]
AVAILABLE_PRESETS: list[str] = [
RointePreset.ECO,
RointePreset.COMFORT,
RointePreset.ICE,
]

ROINTE_HASS_MAP = {
RointePreset.ECO: PRESET_ECO,
RointePreset.COMFORT: PRESET_COMFORT,
RointePreset.ICE: RointePreset.ICE,
}


async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the radiator climate entity from the config entry."""
coordinator: RointeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]

# Register the Entity classes and platform on the coordinator.
coordinator.add_entities_for_seen_keys(
async_add_entities, [RointeHaClimate], "climate"
)


class RointeHaClimate(RointeRadiatorEntity, ClimateEntity):
"""Climate entity."""

_attr_icon = "mdi:radiator"
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
)
_attr_target_temperature_step = RADIATOR_TEMP_STEP
_attr_hvac_modes = AVAILABLE_HVAC_MODES
_attr_preset_modes = AVAILABLE_PRESETS

_attr_has_entity_name = True
_attr_name = None

def __init__(
self,
radiator: RointeDevice,
coordinator: RointeDataUpdateCoordinator,
) -> None:
"""Init the Climate entity."""

super().__init__(coordinator, radiator, unique_id=radiator.id)

self.entity_description = ClimateEntityDescription(
key="radiator",
name=radiator.name,
)

@property
def target_temperature(self) -> float | None:
"""Return the current temperature or None if the device is off."""

LOGGER.debug(
f"[{self._radiator.name}] :: target_temperature >> Mode: {self._radiator.mode} Power: {self._radiator.power}. Preset: {self._radiator.preset}"
)

if (
self._radiator.mode == RointeOperationMode.MANUAL
and not self._radiator.power
) or (
self._radiator.mode == RointeOperationMode.AUTO
and self._radiator.preset == RointePreset.OFF
):
return None

if self._radiator.mode == RointeOperationMode.AUTO:
if self._radiator.preset == RointePreset.ECO:
return self._radiator.eco_temp
if self._radiator.preset == RointePreset.COMFORT:
return self._radiator.comfort_temp
if self._radiator.preset == RointePreset.ICE:
return self._radiator.ice_temp

return self._radiator.temp

@property
def current_temperature(self) -> float:
"""Get current temperature (Probe)."""
return self._radiator.temp_probe

@property
def max_temp(self) -> float:
"""Max selectable temperature."""
if self._radiator.user_mode_supported and self._radiator.user_mode:
return self._radiator.um_max_temp

return RADIATOR_TEMP_MAX

@property
def min_temp(self) -> float:
"""Minimum selectable temperature."""
if self._radiator.user_mode_supported and self._radiator.user_mode:
return self._radiator.um_min_temp

return RADIATOR_TEMP_MIN

@property
def hvac_mode(self) -> HVACMode:
"""Return the current HVAC mode."""
if not self._radiator.power:
return HVACMode.OFF

if self._radiator.mode == RointeOperationMode.AUTO:
return HVACMode.AUTO

return HVACMode.HEAT

@property
def hvac_action(self) -> HVACAction:
"""Return the current HVAC action."""

# Special mode for AUTO mode and waiting for schedule to activate.
if (
self._radiator.mode == RointeOperationMode.AUTO.value
and self._radiator.preset == HVACMode.OFF
):
return HVACAction.IDLE

# Forced to off, either on Manual or Auto mode.
if not self._radiator.power:
return HVACAction.OFF

# Otherwise, it's heating.
return HVACAction.HEATING

@property
def preset_mode(self) -> str | None:
"""Convert the device's preset to HA preset modes."""

# Also captures "none" (man mode, temperature outside presets)
return ROINTE_HASS_MAP.get(self._radiator.preset, None)

async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""

target_temperature = kwargs["temperature"]

if not RADIATOR_TEMP_MIN <= target_temperature <= RADIATOR_TEMP_MAX:
raise ValueError(
f"Invalid set_humidity value (must be in range 7.0, 30.0): {target_temperature}"
)

# Round to the nearest half value.
rounded_temp = round(target_temperature * 2) / 2

if not await self.device_manager.send_command(
self._radiator, RointeCommand.SET_TEMP, rounded_temp
):
raise HomeAssistantError(
f"Failed to set HVAC mode for {self._radiator.name}"
)

await self._signal_thermostat_update()

async def async_set_hvac_mode(self, hvac_mode):
"""Set new target hvac mode."""

LOGGER.debug("Setting HVAC mode to %s", hvac_mode)

if not await self.device_manager.send_command(
self._radiator, RointeCommand.SET_HVAC_MODE, hvac_mode
):
raise HomeAssistantError(
f"Failed to set HVAC mode for {self._radiator.name}"
)

await self._signal_thermostat_update()

async def async_set_preset_mode(self, preset_mode):
"""Set new target preset mode."""
LOGGER.debug("Setting preset mode: %s", preset_mode)

if not await self.device_manager.send_command(
self._radiator, RointeCommand.SET_PRESET, preset_mode
):
raise HomeAssistantError(
f"Failed to set HVAC mode for {self._radiator.name}"
)

await self._signal_thermostat_update()

async def _signal_thermostat_update(self):
"""Signal a radiator change."""

# Update the data
await self.coordinator.async_refresh()
self.async_write_ha_state()
Loading

0 comments on commit 20936eb

Please sign in to comment.