Skip to content

Commit

Permalink
Rointe Integration
Browse files Browse the repository at this point in the history
  • Loading branch information
tggm committed Dec 7, 2022
1 parent e7a0604 commit e8e09ee
Show file tree
Hide file tree
Showing 20 changed files with 1,586 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,8 @@ build.json @home-assistant/supervisor
/tests/components/rituals_perfume_genie/ @milanmeu
/homeassistant/components/rmvtransport/ @cgtobi
/tests/components/rmvtransport/ @cgtobi
/homeassistant/components/rointe/ @tggm
/tests/components/rointe/ @tggm
/homeassistant/components/roku/ @ctalkington
/tests/components/roku/ @ctalkington
/homeassistant/components/roomba/ @pschmitt @cyr-ius @shenxn
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/rointe/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.code-workspace
104 changes: 104 additions & 0 deletions homeassistant/components/rointe/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""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_LOCAL_ID,
CONF_PASSWORD,
CONF_USERNAME,
DOMAIN,
LOGGER,
PLATFORMS,
ROINTE_API_MANAGER,
ROINTE_COORDINATOR,
ROINTE_DEVICE_MANAGER,
ROINTE_HA_DEVICES,
ROINTE_HA_ROINTE_MAP,
)
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."""

try:
(
rointe_device_manager,
rointe_api,
rointe_coordinator,
) = await init_device_manager(hass, entry)
except ConfigEntryNotReady as ex:
LOGGER.error(
"An error occurred while setting up the Rointe Integration: %s", ex
)
raise
else:
# Initialize Hass data if necessary.
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
ROINTE_HA_ROINTE_MAP: {},
ROINTE_HA_DEVICES: set(),
}

hass.data[DOMAIN][entry.entry_id][ROINTE_DEVICE_MANAGER] = rointe_device_manager
hass.data[DOMAIN][entry.entry_id][ROINTE_API_MANAGER] = rointe_api
hass.data[DOMAIN][entry.entry_id][ROINTE_COORDINATOR] = rointe_coordinator

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry and removes event handlers."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

if unload_ok:
rointe_data = hass.data[DOMAIN].pop(entry.entry_id)

while rointe_data[ROINTE_COORDINATOR].cleanup_callbacks:
rointe_data[ROINTE_COORDINATOR].cleanup_callbacks.pop()()

return unload_ok


async def init_device_manager(
hass: HomeAssistant, entry: ConfigEntry
) -> tuple[RointeDeviceManager, RointeAPI, RointeDataUpdateCoordinator]:
"""Initialize the device manager, API and coordinator."""

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

LOGGER.debug("Device manager: Logging in")

# 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],
local_id=entry.data[CONF_LOCAL_ID],
hass=hass,
rointe_api=rointe_api,
)

LOGGER.debug("Device manager: Initializing Data Coordinator")
rointe_coordinator = RointeDataUpdateCoordinator(hass, rointe_device_manager)

LOGGER.debug("Device manager: Calling first refresh")
await rointe_coordinator.async_config_entry_first_refresh()

LOGGER.debug("Device manager: Setting up platforms")
hass.config_entries.async_setup_platforms(entry, PLATFORMS)

return rointe_device_manager, rointe_api, rointe_coordinator
256 changes: 256 additions & 0 deletions homeassistant/components/rointe/climate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
"""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 TEMP_CELSIUS
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import (
CMD_SET_HVAC_MODE,
CMD_SET_PRESET,
CMD_SET_TEMP,
DOMAIN,
LOGGER,
PRESET_ROINTE_ICE,
ROINTE_COORDINATOR,
)
from .coordinator import RointeDataUpdateCoordinator
from .rointe_entity import RointeRadiatorEntity


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][
ROINTE_COORDINATOR
]

LOGGER.debug("Climate Platform: Async setup entry")

# 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."""

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

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

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

@property
def icon(self) -> str | None:
"""Icon of the entity."""
return "mdi:radiator"

@property
def temperature_unit(self) -> str:
"""Temperature unit."""
return TEMP_CELSIUS

@property
def target_temperature(self) -> float:
"""Return the current temperature."""
if self._radiator.mode == "auto":
if self._radiator.preset == "eco":
return self._radiator.eco_temp
if self._radiator.preset == "comfort":
return self._radiator.comfort_temp
if self._radiator.preset == "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 30.0

@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 7.0

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

return 30.0

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

return 7.0

@property
def supported_features(self) -> ClimateEntityFeature:
"""Flag supported features."""
return (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
)

@property
def target_temperature_step(self) -> float | None:
"""Temperature step."""
return 0.5

@property
def hvac_modes(self) -> list[str]:
"""Return hvac modes available."""
return [HVACMode.OFF, HVACMode.HEAT, HVACMode.AUTO]

@property
def preset_modes(self) -> list[str]:
"""Return the available preset modes."""
return [PRESET_COMFORT, PRESET_ECO, PRESET_ROINTE_ICE]

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

if self._radiator.mode == "auto":
return HVACMode.AUTO

return HVACMode.HEAT

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

# Special mode for AUTO mode and waiting for schedule to activate.
if self._radiator.mode == "auto" and self._radiator.preset == "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."""

if self._radiator.preset == "eco":
return PRESET_ECO
if self._radiator.preset == "comfort":
return PRESET_COMFORT
if self._radiator.preset == "ice":
return PRESET_ROINTE_ICE

# Also captures "none" (man mode, temperature outside presets)
return None

async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
target_temperature = float(kwargs["temperature"])

if not await self.device_manager.send_command(
self._radiator, CMD_SET_TEMP, target_temperature
):
LOGGER.error(
"Failed to set Temperature [%s] for [%s]",
target_temperature,
self._radiator.name,
)

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, CMD_SET_HVAC_MODE, hvac_mode
):
LOGGER.error(
"Failed to set HVAC mode [%s] for [%s]", hvac_mode, self._radiator.name
)

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, CMD_SET_PRESET, preset_mode
):
LOGGER.error(
"Failed to set preset mode [%s] for [%s]",
preset_mode,
self._radiator.name,
)

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
LOGGER.debug("_signal_thermostat_update")
await self.coordinator.async_request_refresh()
self.async_write_ha_state()
Loading

0 comments on commit e8e09ee

Please sign in to comment.