Skip to content

Commit d65f0ab

Browse files
Add Aquarea support and fix vendor library integration
1 parent c9d684c commit d65f0ab

File tree

9 files changed

+271
-25
lines changed

9 files changed

+271
-25
lines changed

custom_components/panasonic_cc/__init__.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Platform for the Panasonic Comfort Cloud."""
2+
23
import logging
34
from typing import Dict
45

@@ -15,6 +16,7 @@
1516
from homeassistant.loader import async_get_integration
1617
from aio_panasonic_comfort_cloud import ApiClient
1718
from aioaquarea import Client as AquareaApiClient, AquareaEnvironment
19+
from aioaquarea.errors import AuthenticationError
1820

1921
from .const import (
2022
CONF_UPDATE_INTERVAL_VERSION,
@@ -120,24 +122,34 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
120122

121123
if api.has_unknown_devices or AQUAREA_DEMO:
122124
try:
123-
125+
_LOGGER.info("Starting Aquarea setup...")
124126
if not AQUAREA_DEMO:
127+
_LOGGER.info("Creating Aquarea API client with real credentials")
125128
aquarea_api_client = AquareaApiClient(client, username, password)
126129
await aquarea_api_client.login()
127130
else:
131+
_LOGGER.info("Creating Aquarea API client in DEMO mode")
128132
aquarea_api_client = AquareaApiClient(client, environment=AquareaEnvironment.DEMO)
129-
aquarea_api_client._access_token = 'dummy'
130-
aquarea_api_client._token_expiration = None
131-
aquarea_devices = await aquarea_api_client.get_devices(include_long_id=True)
133+
aquarea_api_client._api_client.access_token = 'dummy'
134+
aquarea_api_client._api_client.token_expiration = None
135+
_LOGGER.info("Fetching Aquarea devices...")
136+
aquarea_devices = await aquarea_api_client.get_devices()
137+
_LOGGER.info(f"Found {len(aquarea_devices)} Aquarea device(s)")
132138
for aquarea_device in aquarea_devices:
133139
try:
140+
_LOGGER.info(f"Setting up Aquarea device: {aquarea_device.name}")
134141
aquarea_device_coordinator = AquareaDeviceCoordinator(hass, conf, aquarea_api_client, aquarea_device)
135142
await aquarea_device_coordinator.async_config_entry_first_refresh()
136143
aquarea_coordinators.append(aquarea_device_coordinator)
144+
_LOGGER.info(f"Successfully setup Aquarea device: {aquarea_device.name}")
137145
except Exception as e:
138-
_LOGGER.warning(f"Failed to setup Aquarea device: {aquarea_device.name} ({e})", exc_info=e)
146+
_LOGGER.error(f"Failed to setup Aquarea device: {aquarea_device.name} ({e})", exc_info=e)
147+
except AuthenticationError as e:
148+
_LOGGER.error(f"Aquarea authentication failed (2FA may be required): {e}", exc_info=e)
149+
_LOGGER.error("The Aquarea API currently does not support 2FA/MFA verification")
150+
_LOGGER.error("Please check your Panasonic account settings and disable 2FA if needed")
139151
except Exception as e:
140-
_LOGGER.warning(f"Failed to setup Aquarea: {e}", exc_info=e)
152+
_LOGGER.error(f"Failed to setup Aquarea: {e}", exc_info=e)
141153

142154

143155
hass.data[DOMAIN][DATA_COORDINATORS] = data_coordinators

custom_components/panasonic_cc/button.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,18 @@ class PanasonicButtonEntityDescription(ButtonEntityDescription):
1818
func: Callable[[PanasonicDeviceCoordinator], Awaitable[Any]] | None = None
1919

2020

21+
async def _update_app_version(coordinator: PanasonicDeviceCoordinator) -> None:
22+
"""Update app version."""
23+
app_version = getattr(coordinator.api_client, '_app_version', None)
24+
if app_version and hasattr(app_version, 'refresh'):
25+
await app_version.refresh()
26+
2127
APP_VERSION_DESCRIPTION = PanasonicButtonEntityDescription(
2228
key="update_app_version",
2329
name="Fetch latest app version",
2430
icon="mdi:refresh",
2531
entity_category=EntityCategory.DIAGNOSTIC,
26-
func = lambda coordinator: coordinator.api_client.update_app_version()
32+
func = _update_app_version
2733
)
2834

2935
UPDATE_DATA_DESCRIPTION = ButtonEntityDescription(

custom_components/panasonic_cc/const.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
PANASONIC_DEVICES = "panasonic_devices"
8989
DATA_COORDINATORS = "data_coordinators"
9090
ENERGY_COORDINATORS = "energy_coordinators"
91-
AQUAREA_COORDINATORS = "aquarea_coorinators"
91+
AQUAREA_COORDINATORS = "aquarea_coordinators"
9292

9393
COMPONENT_TYPES = [
9494
Platform.CLIMATE,

custom_components/panasonic_cc/coordinator.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,15 @@ def device_id(self) -> str:
5151

5252
@property
5353
def device_info(self)->DeviceInfo:
54+
# Access app_version via private attribute
55+
app_version = getattr(self._api_client, '_app_version', None)
56+
version = app_version.version if app_version and hasattr(app_version, 'version') else '1.0.0'
5457
return DeviceInfo(
5558
identifiers={(DOMAIN, self._panasonic_device_info.id )},
5659
manufacturer=MANUFACTURER,
5760
model=self._panasonic_device_info.model,
5861
name=self._panasonic_device_info.name,
59-
sw_version=self._api_client.app_version
62+
sw_version=version
6063
)
6164

6265
def get_change_request_builder(self):
@@ -126,12 +129,15 @@ def energy(self) -> PanasonicDeviceEnergy | None:
126129

127130
@property
128131
def device_info(self)->DeviceInfo:
132+
# Access app_version via private attribute
133+
app_version = getattr(self._api_client, '_app_version', None)
134+
version = app_version.version if app_version and hasattr(app_version, 'version') else '1.0.0'
129135
return DeviceInfo(
130136
identifiers={(DOMAIN, self._panasonic_device_info.id )},
131137
manufacturer=MANUFACTURER,
132138
model=self._panasonic_device_info.model,
133139
name=self._panasonic_device_info.name,
134-
sw_version=self._api_client.app_version
140+
sw_version=version
135141
)
136142

137143
async def _fetch_device_data(self)->int:
@@ -165,7 +171,13 @@ def __init__(self, hass: HomeAssistant, config: dict, api_client: AquareaApiClie
165171
self._aquarea_device_info = device_info
166172
self._device:AquareaDevice | None = None
167173
self._update_id = 0
168-
self._is_demo = api_client._environment == AquareaEnvironment.DEMO
174+
# Access environment via private attribute (_environment) from client
175+
self._is_demo = self._get_environment() == AquareaEnvironment.DEMO
176+
177+
def _get_environment(self) -> AquareaEnvironment:
178+
"""Get environment from client."""
179+
# Access private attribute _environment to avoid modifying vendor library
180+
return getattr(self._api_client, '_environment', AquareaEnvironment.PRODUCTION)
169181

170182
@property
171183
def device(self) -> AquareaDevice:
@@ -188,8 +200,8 @@ def device_info(self)->DeviceInfo:
188200
identifiers={(DOMAIN, self.device_id)},
189201
manufacturer=self.device.manufacturer,
190202
model="",
191-
name=self.device.name,
192-
sw_version=self.device.version,
203+
name=self.device.device_name,
204+
sw_version=self.device.firmware_version,
193205
)
194206

195207
async def _fetch_device_data(self)->int:

custom_components/panasonic_cc/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@
1010
"integration_type": "hub",
1111
"iot_class": "cloud_polling",
1212
"issue_tracker": "https://github.com/sockless-coding/panasonic_cc/issues",
13-
"requirements": ["aiohttp","aio-panasonic-comfort-cloud==2025.5.1","aioaquarea==0.7.2"],
13+
"requirements": ["aiohttp","aio-panasonic-comfort-cloud==2025.5.1","aioaquarea==1.0.0"],
1414
"quality_scale": "silver"
1515
}

custom_components/panasonic_cc/number.py

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
from typing import Callable
33
from dataclasses import dataclass
44

5-
from homeassistant.const import PERCENTAGE
5+
from homeassistant.const import PERCENTAGE, UnitOfTemperature
6+
7+
_LOGGER = logging.getLogger(__name__)
68
from homeassistant.core import HomeAssistant
79
from homeassistant.components.number import (
810
NumberDeviceClass,
@@ -12,18 +14,25 @@
1214
)
1315

1416
from aio_panasonic_comfort_cloud import PanasonicDevice, PanasonicDeviceZone, ChangeRequestBuilder
17+
from aioaquarea import Device as AquareaDevice
18+
from aioaquarea import ExtendedOperationMode
1519

1620
from . import DOMAIN
17-
from .const import DATA_COORDINATORS
18-
from .coordinator import PanasonicDeviceCoordinator
19-
from .base import PanasonicDataEntity
21+
from .const import DATA_COORDINATORS, AQUAREA_COORDINATORS
22+
from .coordinator import PanasonicDeviceCoordinator, AquareaDeviceCoordinator
23+
from .base import PanasonicDataEntity, AquareaDataEntity
2024

2125
@dataclass(frozen=True, kw_only=True)
2226
class PanasonicNumberEntityDescription(NumberEntityDescription):
2327
"""Describes Panasonic Number entity."""
2428
get_value: Callable[[PanasonicDevice], int]
2529
set_value: Callable[[ChangeRequestBuilder, int], ChangeRequestBuilder]
2630

31+
@dataclass(frozen=True, kw_only=True)
32+
class AquareaNumberEntityDescription(NumberEntityDescription):
33+
"""Describes Aquarea Number entity."""
34+
zone_id: int
35+
2736
def create_zone_damper_description(zone: PanasonicDeviceZone):
2837
return PanasonicNumberEntityDescription(
2938
key = f"zone-{zone.id}-damper",
@@ -49,9 +58,51 @@ async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities):
4958
data_coordinator,
5059
create_zone_damper_description(zone)))
5160

61+
# Aquarea Number entities for temperature targets
62+
aquarea_coordinators: list[AquareaDeviceCoordinator] = hass.data[DOMAIN][AQUAREA_COORDINATORS]
63+
for coordinator in aquarea_coordinators:
64+
for zone_id in coordinator.device.zones:
65+
zone = coordinator.device.zones.get(zone_id)
66+
# Add heat target temperature
67+
devices.append(AquareaNumberEntity(
68+
coordinator,
69+
AquareaNumberEntityDescription(
70+
zone_id=zone_id,
71+
key=f"zone-{zone_id}-heat-target",
72+
translation_key=f"zone-{zone_id}-heat-target",
73+
name=f"{zone.name} Heat Target",
74+
icon="mdi:thermometer",
75+
device_class=NumberDeviceClass.TEMPERATURE,
76+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
77+
native_max_value=zone.heat_max if zone.heat_max is not None else 30,
78+
native_min_value=zone.heat_min if zone.heat_min is not None else 10,
79+
native_step=1,
80+
mode=NumberMode.BOX,
81+
)
82+
))
83+
# Add cool target temperature if supported
84+
if zone.cool_max and zone.cool_min:
85+
devices.append(AquareaNumberEntity(
86+
coordinator,
87+
AquareaNumberEntityDescription(
88+
zone_id=zone_id,
89+
key=f"zone-{zone_id}-cool-target",
90+
translation_key=f"zone-{zone_id}-cool-target",
91+
name=f"{zone.name} Cool Target",
92+
icon="mdi:thermometer",
93+
device_class=NumberDeviceClass.TEMPERATURE,
94+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
95+
native_max_value=zone.cool_max if zone.cool_max is not None else 30,
96+
native_min_value=zone.cool_min if zone.cool_min is not None else 16,
97+
native_step=1,
98+
mode=NumberMode.BOX,
99+
)
100+
))
101+
52102
async_add_entities(devices)
53103

54104
class PanasonicNumberEntity(PanasonicDataEntity, NumberEntity):
105+
"""Representation of a Panasonic Number."""
55106

56107
entity_description: PanasonicNumberEntityDescription
57108

@@ -70,4 +121,57 @@ async def async_set_native_value(self, value: float) -> None:
70121
self.async_write_ha_state()
71122

72123
def _async_update_attrs(self) -> None:
73-
self._attr_native_value = self.entity_description.get_value(self.coordinator.device)
124+
self._attr_native_value = self.entity_description.get_value(self.coordinator.device)
125+
126+
127+
class AquareaNumberEntity(AquareaDataEntity, NumberEntity):
128+
"""Aquarea Number entity for setting target temperatures."""
129+
130+
entity_description: AquareaNumberEntityDescription
131+
132+
def __init__(self, coordinator: AquareaDeviceCoordinator, description: AquareaNumberEntityDescription):
133+
"""Initialize the number entity."""
134+
self.entity_description = description
135+
super().__init__(coordinator, description.key)
136+
137+
async def async_set_native_value(self, value: float) -> None:
138+
"""Set new target temperature value."""
139+
zone_id = self.entity_description.zone_id
140+
temperature = int(value)
141+
142+
# Determine if we're setting heat or cool based on key
143+
if "heat-target" in self.entity_description.key:
144+
# Temporarily switch mode to HEAT if needed
145+
original_mode = self.coordinator.device.mode
146+
if original_mode not in (ExtendedOperationMode.HEAT, ExtendedOperationMode.AUTO_HEAT):
147+
_LOGGER.debug(f"Switching to HEAT mode to set heat target for zone {zone_id}")
148+
await self.coordinator.device.set_temperature(temperature, zone_id)
149+
elif "cool-target" in self.entity_description.key:
150+
# Temporarily switch mode to COOL if needed
151+
original_mode = self.coordinator.device.mode
152+
if original_mode not in (ExtendedOperationMode.COOL, ExtendedOperationMode.AUTO_COOL):
153+
_LOGGER.debug(f"Switching to COOL mode to set cool target for zone {zone_id}")
154+
await self.coordinator.device.set_temperature(temperature, zone_id)
155+
156+
self._attr_native_value = temperature
157+
self.async_write_ha_state()
158+
await self.coordinator.async_request_refresh()
159+
160+
def _async_update_attrs(self) -> None:
161+
"""Update the current value."""
162+
zone = self.coordinator.device.zones.get(self.entity_description.zone_id)
163+
if zone:
164+
# Si heatSet/coolSet n'est pas disponible dans l'API,
165+
# on affiche la température actuelle comme référence
166+
if "heat-target" in self.entity_description.key:
167+
# Try to get target temperature first, fallback to current temperature
168+
temp = zone.heat_target_temperature
169+
if temp is None:
170+
# L'API ne retourne pas la consigne, on utilise la temp actuelle comme ref
171+
temp = zone.temperature
172+
self._attr_native_value = temp
173+
elif "cool-target" in self.entity_description.key:
174+
temp = zone.cool_target_temperature
175+
if temp is None:
176+
temp = zone.temperature
177+
self._attr_native_value = temp

0 commit comments

Comments
 (0)