Skip to content
This repository was archived by the owner on Mar 19, 2024. It is now read-only.

Commit decf43c

Browse files
Senso compatibility (#194)
1 parent c69f5de commit decf43c

File tree

26 files changed

+194
-84
lines changed

26 files changed

+194
-84
lines changed

README.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,19 @@
77

88
Ideas are welcome ! Don't hesitate to create issue to suggest something, it will be really appreciated.
99

10-
**This integration is NOT compatible with sensoAPP, only with multiMATIC app.**
11-
12-
**This integration is NOT likely to be compatible with VR921 (even if you use multiMATIC app). You may still have
13-
some entities, but not all.**
10+
**This integration is also compatible with sensoAPP and has been tested with the vr920 and vr921 devices.**
1411

1512
## Installations
1613
- Through HACS [custom repositories](https://hacs.xyz/docs/faq/custom_repositories/) !
1714
- Otherwise, download the zip from the latest release and copy `multimatic` folder and put it inside your `custom_components` folder.
1815

1916
You can configure it through the UI using integration.
20-
You have to provide your username and password (same as multimatic app), if you have multiple serial numbers, you can choose for which number serial number you want the integration.
17+
You have to provide your username and password (same as multimatic or senso app), if you have multiple serial numbers, you can choose for which number serial number you want the integration.
2118
You can create multiple instance of the integration with different serial number (**This is still a beta feature**).
2219

2320
**It is strongly recommended using a dedicated user for HA**, for 2 reasons:
2421
- As usual for security reason, if your HA got compromised somehow, you know which user to block
25-
- I cannot confirm it, but it seems multimatic API only accept the same user to be connected at the same time
22+
- I cannot confirm it, but it seems multimatic and senso API only accept the same user to be connected at the same time
2623

2724
## Changelog
2825
See [releases details](https://github.com/thomasgermain/vaillant-component/releases)

custom_components/multimatic/__init__.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_STOP
88
from homeassistant.core import HomeAssistant
99
from homeassistant.helpers.typing import ConfigType
10+
from pymultimatic.api import defaults
1011

1112
from .const import (
1213
CONF_SERIAL_NUMBER,
@@ -15,7 +16,7 @@
1516
DEFAULT_SCAN_INTERVAL,
1617
DOMAIN,
1718
PLATFORMS,
18-
SERVICES_HANDLER,
19+
SERVICES_HANDLER, CONF_APPLICATION,
1920
)
2021
from .coordinator import MultimaticApi, MultimaticCoordinator
2122
from .service import SERVICES, MultimaticServiceHandler
@@ -127,3 +128,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
127128
_LOGGER.debug("Remaining data for multimatic %s", hass.data[DOMAIN])
128129

129130
return unload_ok
131+
132+
133+
async def async_migrate_entry(hass, config_entry: ConfigEntry):
134+
"""Migrate old entry."""
135+
_LOGGER.debug("Migrating from version %s", config_entry.version)
136+
if config_entry.version == 1:
137+
new = {**config_entry.data, CONF_APPLICATION: defaults.MULTIMATIC}
138+
139+
config_entry.version = 2
140+
hass.config_entries.async_update_entry(config_entry, data=new)
141+
142+
_LOGGER.debug("Migration to version %s successful", config_entry.version)
143+
144+
return True

custom_components/multimatic/climate.py

Lines changed: 125 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Interfaces with Multimatic climate."""
22
from __future__ import annotations
33

4-
import abc
4+
from abc import ABC, abstractmethod
55
from collections.abc import Mapping
66
import logging
77
from typing import Any
@@ -49,8 +49,10 @@
4949
PRESET_QUICK_VETO,
5050
PRESET_SYSTEM_OFF,
5151
ROOMS,
52+
SENSO,
5253
VENTILATION,
5354
ZONES,
55+
CONF_APPLICATION,
5456
)
5557
from .coordinator import MultimaticCoordinator
5658
from .entities import MultimaticEntity
@@ -74,11 +76,12 @@ async def async_setup_entry(
7476
zones_coo = get_coordinator(hass, ZONES, entry.entry_id)
7577
rooms_coo = get_coordinator(hass, ROOMS, entry.entry_id)
7678
ventilation_coo = get_coordinator(hass, VENTILATION, entry.entry_id)
79+
system_application = SENSO if entry.data[CONF_APPLICATION] == SENSO else MULTIMATIC
7780

7881
if zones_coo.data:
7982
for zone in zones_coo.data:
8083
if not zone.rbr and zone.enabled:
81-
climates.append(ZoneClimate(zones_coo, zone, ventilation_coo.data))
84+
climates.append(build_zone_climate(zones_coo, zone, ventilation_coo.data, system_application))
8285

8386
if rooms_coo.data:
8487
rbr_zone = next((zone for zone in zones_coo.data if zone.rbr), None)
@@ -103,7 +106,7 @@ async def async_setup_entry(
103106
)
104107

105108

106-
class MultimaticClimate(MultimaticEntity, ClimateEntity, abc.ABC):
109+
class MultimaticClimate(MultimaticEntity, ClimateEntity, ABC):
107110
"""Base class for climate."""
108111

109112
def __init__(
@@ -131,7 +134,7 @@ def active_mode(self) -> ActiveMode:
131134
return self.coordinator.api.get_active_mode(self.component)
132135

133136
@property
134-
@abc.abstractmethod
137+
@abstractmethod
135138
def component(self) -> Component:
136139
"""Return the room or the zone."""
137140

@@ -362,52 +365,22 @@ def current_humidity(self) -> int | None:
362365
return int(humidity) if humidity is not None else None
363366

364367

365-
class ZoneClimate(MultimaticClimate):
366-
"""Climate for a zone."""
368+
def build_zone_climate(coordinator: MultimaticCoordinator, zone: Zone, ventilation, application) -> AbstractZoneClimate:
369+
if application == MULTIMATIC:
370+
return ZoneClimate(coordinator, zone, ventilation)
371+
return ZoneClimateSenso(coordinator, zone, ventilation)
367372

368-
_MULTIMATIC_TO_HA: dict[Mode, list] = {
369-
OperatingModes.AUTO: [HVACMode.AUTO, PRESET_COMFORT],
370-
OperatingModes.DAY: [None, PRESET_DAY],
371-
OperatingModes.NIGHT: [None, PRESET_SLEEP],
372-
OperatingModes.OFF: [HVACMode.OFF, PRESET_NONE],
373-
OperatingModes.ON: [None, PRESET_COOLING_ON],
374-
OperatingModes.QUICK_VETO: [None, PRESET_QUICK_VETO],
375-
QuickModes.ONE_DAY_AT_HOME: [HVACMode.AUTO, PRESET_HOME],
376-
QuickModes.PARTY: [None, PRESET_PARTY],
377-
QuickModes.VENTILATION_BOOST: [HVACMode.FAN_ONLY, PRESET_NONE],
378-
QuickModes.ONE_DAY_AWAY: [HVACMode.OFF, PRESET_AWAY],
379-
QuickModes.SYSTEM_OFF: [HVACMode.OFF, PRESET_SYSTEM_OFF],
380-
QuickModes.HOLIDAY: [HVACMode.OFF, PRESET_HOLIDAY],
381-
QuickModes.COOLING_FOR_X_DAYS: [None, PRESET_COOLING_FOR_X_DAYS],
382-
}
383-
384-
_HA_MODE_TO_MULTIMATIC = {
385-
HVACMode.AUTO: OperatingModes.AUTO,
386-
HVACMode.OFF: OperatingModes.OFF,
387-
HVACMode.FAN_ONLY: QuickModes.VENTILATION_BOOST,
388-
HVACMode.COOL: QuickModes.COOLING_FOR_X_DAYS,
389-
}
390-
391-
_HA_PRESET_TO_MULTIMATIC = {
392-
PRESET_COMFORT: OperatingModes.AUTO,
393-
PRESET_DAY: OperatingModes.DAY,
394-
PRESET_SLEEP: OperatingModes.NIGHT,
395-
PRESET_COOLING_ON: OperatingModes.ON,
396-
PRESET_HOME: QuickModes.ONE_DAY_AT_HOME,
397-
PRESET_PARTY: QuickModes.PARTY,
398-
PRESET_AWAY: QuickModes.ONE_DAY_AWAY,
399-
PRESET_SYSTEM_OFF: QuickModes.SYSTEM_OFF,
400-
PRESET_COOLING_FOR_X_DAYS: QuickModes.COOLING_FOR_X_DAYS,
401-
}
402373

374+
class AbstractZoneClimate(MultimaticClimate, ABC):
375+
"""Abstract class for a climate for a zone."""
403376
def __init__(
404377
self, coordinator: MultimaticCoordinator, zone: Zone, ventilation
405378
) -> None:
406379
"""Initialize entity."""
407380
super().__init__(coordinator, zone.id)
408381

409-
self._supported_hvac = list(ZoneClimate._HA_MODE_TO_MULTIMATIC.keys())
410-
self._supported_presets = list(ZoneClimate._HA_PRESET_TO_MULTIMATIC.keys())
382+
self._supported_hvac = list(self._ha_mode().keys())
383+
self._supported_presets = list(self._ha_preset().keys())
411384

412385
if not zone.cooling:
413386
self._supported_presets.remove(PRESET_COOLING_ON)
@@ -419,6 +392,18 @@ def __init__(
419392

420393
self._zone_id = zone.id
421394

395+
@abstractmethod
396+
def _ha_mode(self):
397+
pass
398+
399+
@abstractmethod
400+
def _multimatic_mode(self):
401+
pass
402+
403+
@abstractmethod
404+
def _ha_preset(self):
405+
pass
406+
422407
@property
423408
def extra_state_attributes(self) -> Mapping[str, Any] | None:
424409
"""Return entity specific state attributes."""
@@ -438,7 +423,7 @@ def component(self) -> Zone:
438423
def hvac_mode(self) -> HVACMode:
439424
"""Get the hvac mode based on multimatic mode."""
440425
current_mode = self.active_mode.current
441-
hvac_mode = ZoneClimate._MULTIMATIC_TO_HA[current_mode][0]
426+
hvac_mode = self._multimatic_mode()[current_mode][0]
442427
if not hvac_mode:
443428
if (
444429
current_mode
@@ -497,7 +482,7 @@ async def async_set_temperature(self, **kwargs: Any) -> None:
497482

498483
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
499484
"""Set new target hvac mode."""
500-
mode = ZoneClimate._HA_MODE_TO_MULTIMATIC[hvac_mode]
485+
mode = self._ha_mode()[hvac_mode]
501486
await self.coordinator.api.set_zone_operating_mode(self, mode)
502487

503488
@property
@@ -511,7 +496,7 @@ def hvac_action(self) -> str | None:
511496
@property
512497
def preset_mode(self) -> str | None:
513498
"""Return the current preset mode, e.g., home, away, temp."""
514-
return ZoneClimate._MULTIMATIC_TO_HA[self.active_mode.current][1]
499+
return self._multimatic_mode()[self.active_mode.current][1]
515500

516501
@property
517502
def preset_modes(self) -> list[str] | None:
@@ -522,5 +507,99 @@ def preset_modes(self) -> list[str] | None:
522507

523508
async def async_set_preset_mode(self, preset_mode: str) -> None:
524509
"""Set new target preset mode."""
525-
mode = ZoneClimate._HA_PRESET_TO_MULTIMATIC[preset_mode]
510+
mode = self._ha_preset()[preset_mode]
526511
await self.coordinator.api.set_zone_operating_mode(self, mode)
512+
513+
class ZoneClimate(AbstractZoneClimate):
514+
"""Climate for a MULTIMATIC zone."""
515+
516+
_MULTIMATIC_TO_HA: dict[Mode, list] = {
517+
OperatingModes.AUTO: [HVACMode.AUTO, PRESET_COMFORT],
518+
OperatingModes.DAY: [None, PRESET_DAY],
519+
OperatingModes.NIGHT: [None, PRESET_SLEEP],
520+
OperatingModes.OFF: [HVACMode.OFF, PRESET_NONE],
521+
OperatingModes.ON: [None, PRESET_COOLING_ON],
522+
OperatingModes.QUICK_VETO: [None, PRESET_QUICK_VETO],
523+
QuickModes.ONE_DAY_AT_HOME: [HVACMode.AUTO, PRESET_HOME],
524+
QuickModes.PARTY: [None, PRESET_PARTY],
525+
QuickModes.VENTILATION_BOOST: [HVACMode.FAN_ONLY, PRESET_NONE],
526+
QuickModes.ONE_DAY_AWAY: [HVACMode.OFF, PRESET_AWAY],
527+
QuickModes.SYSTEM_OFF: [HVACMode.OFF, PRESET_SYSTEM_OFF],
528+
QuickModes.HOLIDAY: [HVACMode.OFF, PRESET_HOLIDAY],
529+
QuickModes.COOLING_FOR_X_DAYS: [None, PRESET_COOLING_FOR_X_DAYS],
530+
}
531+
532+
_HA_MODE_TO_MULTIMATIC = {
533+
HVACMode.AUTO: OperatingModes.AUTO,
534+
HVACMode.OFF: OperatingModes.OFF,
535+
HVACMode.FAN_ONLY: QuickModes.VENTILATION_BOOST,
536+
HVACMode.COOL: QuickModes.COOLING_FOR_X_DAYS,
537+
}
538+
539+
_HA_PRESET_TO_MULTIMATIC = {
540+
PRESET_COMFORT: OperatingModes.AUTO,
541+
PRESET_DAY: OperatingModes.DAY,
542+
PRESET_SLEEP: OperatingModes.NIGHT,
543+
PRESET_COOLING_ON: OperatingModes.ON,
544+
PRESET_HOME: QuickModes.ONE_DAY_AT_HOME,
545+
PRESET_PARTY: QuickModes.PARTY,
546+
PRESET_AWAY: QuickModes.ONE_DAY_AWAY,
547+
PRESET_SYSTEM_OFF: QuickModes.SYSTEM_OFF,
548+
PRESET_COOLING_FOR_X_DAYS: QuickModes.COOLING_FOR_X_DAYS,
549+
}
550+
551+
def _ha_mode(self):
552+
return ZoneClimate._HA_MODE_TO_MULTIMATIC
553+
554+
def _multimatic_mode(self):
555+
return ZoneClimate._MULTIMATIC_TO_HA
556+
557+
def _ha_preset(self):
558+
return ZoneClimate._HA_PRESET_TO_MULTIMATIC
559+
560+
561+
class ZoneClimateSenso(AbstractZoneClimate):
562+
"""Climate for a SENSO zone."""
563+
564+
_SENSO_TO_HA: dict[Mode, list] = {
565+
OperatingModes.TIME_CONTROLLED: [HVACMode.AUTO, PRESET_COMFORT],
566+
OperatingModes.DAY: [None, PRESET_DAY],
567+
OperatingModes.NIGHT: [None, PRESET_SLEEP],
568+
OperatingModes.OFF: [HVACMode.OFF, PRESET_NONE],
569+
OperatingModes.MANUAL: [None, PRESET_COOLING_ON],
570+
OperatingModes.QUICK_VETO: [None, PRESET_QUICK_VETO],
571+
QuickModes.ONE_DAY_AT_HOME: [HVACMode.AUTO, PRESET_HOME],
572+
QuickModes.PARTY: [None, PRESET_PARTY],
573+
QuickModes.VENTILATION_BOOST: [HVACMode.FAN_ONLY, PRESET_NONE],
574+
QuickModes.ONE_DAY_AWAY: [HVACMode.OFF, PRESET_AWAY],
575+
QuickModes.SYSTEM_OFF: [HVACMode.OFF, PRESET_SYSTEM_OFF],
576+
QuickModes.HOLIDAY: [HVACMode.OFF, PRESET_HOLIDAY],
577+
QuickModes.COOLING_FOR_X_DAYS: [None, PRESET_COOLING_FOR_X_DAYS],
578+
}
579+
_HA_MODE_TO_SENSO = {
580+
HVACMode.AUTO: OperatingModes.TIME_CONTROLLED,
581+
HVACMode.OFF: OperatingModes.OFF,
582+
HVACMode.FAN_ONLY: QuickModes.VENTILATION_BOOST,
583+
HVACMode.COOL: QuickModes.COOLING_FOR_X_DAYS,
584+
}
585+
586+
_HA_PRESET_TO_SENSO = {
587+
PRESET_COMFORT: OperatingModes.TIME_CONTROLLED,
588+
PRESET_DAY: OperatingModes.DAY,
589+
PRESET_SLEEP: OperatingModes.NIGHT,
590+
PRESET_COOLING_ON: OperatingModes.MANUAL,
591+
PRESET_HOME: QuickModes.ONE_DAY_AT_HOME,
592+
PRESET_PARTY: QuickModes.PARTY,
593+
PRESET_AWAY: QuickModes.ONE_DAY_AWAY,
594+
PRESET_SYSTEM_OFF: QuickModes.SYSTEM_OFF,
595+
PRESET_COOLING_FOR_X_DAYS: QuickModes.COOLING_FOR_X_DAYS,
596+
}
597+
598+
def _ha_mode(self):
599+
return ZoneClimateSenso._HA_MODE_TO_SENSO
600+
601+
def _multimatic_mode(self):
602+
return ZoneClimateSenso._SENSO_TO_HA
603+
604+
def _ha_preset(self):
605+
return ZoneClimateSenso._HA_PRESET_TO_SENSO

custom_components/multimatic/config_flow.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Config flow for multimatic integration."""
22
import logging
33

4-
from pymultimatic.api import ApiError
4+
from pymultimatic.api import ApiError, defaults
55
from pymultimatic.systemmanager import SystemManager
66
import voluptuous as vol
77

@@ -13,7 +13,7 @@
1313
from homeassistant.helpers.aiohttp_client import async_create_clientsession
1414
import homeassistant.helpers.config_validation as cv
1515

16-
from .const import CONF_SERIAL_NUMBER, DEFAULT_SCAN_INTERVAL, DOMAIN
16+
from .const import CONF_SERIAL_NUMBER, DEFAULT_SCAN_INTERVAL, DOMAIN, CONF_APPLICATION
1717

1818
_LOGGER = logging.getLogger(__name__)
1919

@@ -22,6 +22,7 @@
2222
vol.Required(CONF_USERNAME): str,
2323
vol.Required(CONF_PASSWORD): str,
2424
vol.Optional(CONF_SERIAL_NUMBER): str,
25+
vol.Required(CONF_APPLICATION, default="MULTIMATIC"): vol.In(["MULTIMATIC", "SENSO"]),
2526
}
2627
)
2728

@@ -32,16 +33,20 @@ async def validate_input(hass: core.HomeAssistant, data):
3233
Data has the keys from DATA_SCHEMA with values provided by the user.
3334
"""
3435

35-
await validate_authentication(hass, data[CONF_USERNAME], data[CONF_PASSWORD])
36+
await validate_authentication(hass, data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_APPLICATION])
3637

3738
return {"title": "Multimatic"}
3839

3940

40-
async def validate_authentication(hass, username, password):
41+
async def validate_authentication(hass, username, password, application):
4142
"""Ensure provided credentials are working."""
4243
try:
44+
systemApplication = defaults.SENSO if application == "SENSO" else defaults.MULTIMATIC
4345
if not await SystemManager(
44-
username, password, async_create_clientsession(hass)
46+
user=username,
47+
password=password,
48+
session=async_create_clientsession(hass),
49+
application=systemApplication,
4550
).login(True):
4651
raise InvalidAuth
4752
except ApiError as err:
@@ -57,7 +62,7 @@ async def validate_authentication(hass, username, password):
5762
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
5863
"""Handle a config flow for multimatic."""
5964

60-
VERSION = 1
65+
VERSION = 2
6166
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
6267

6368
@staticmethod

0 commit comments

Comments
 (0)