Skip to content
Open
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
24 changes: 18 additions & 6 deletions custom_components/panasonic_cc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Platform for the Panasonic Comfort Cloud."""

import logging
from typing import Dict

Expand All @@ -15,6 +16,7 @@
from homeassistant.loader import async_get_integration
from aio_panasonic_comfort_cloud import ApiClient
from aioaquarea import Client as AquareaApiClient, AquareaEnvironment
from aioaquarea.errors import AuthenticationError

from .const import (
CONF_UPDATE_INTERVAL_VERSION,
Expand Down Expand Up @@ -120,24 +122,34 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):

if api.has_unknown_devices or AQUAREA_DEMO:
try:

_LOGGER.info("Starting Aquarea setup...")
if not AQUAREA_DEMO:
_LOGGER.info("Creating Aquarea API client with real credentials")
aquarea_api_client = AquareaApiClient(client, username, password)
await aquarea_api_client.login()
else:
_LOGGER.info("Creating Aquarea API client in DEMO mode")
aquarea_api_client = AquareaApiClient(client, environment=AquareaEnvironment.DEMO)
aquarea_api_client._access_token = 'dummy'
aquarea_api_client._token_expiration = None
aquarea_devices = await aquarea_api_client.get_devices(include_long_id=True)
aquarea_api_client._api_client.access_token = 'dummy'
aquarea_api_client._api_client.token_expiration = None
_LOGGER.info("Fetching Aquarea devices...")
aquarea_devices = await aquarea_api_client.get_devices()
_LOGGER.info(f"Found {len(aquarea_devices)} Aquarea device(s)")
for aquarea_device in aquarea_devices:
try:
_LOGGER.info(f"Setting up Aquarea device: {aquarea_device.name}")
aquarea_device_coordinator = AquareaDeviceCoordinator(hass, conf, aquarea_api_client, aquarea_device)
await aquarea_device_coordinator.async_config_entry_first_refresh()
aquarea_coordinators.append(aquarea_device_coordinator)
_LOGGER.info(f"Successfully setup Aquarea device: {aquarea_device.name}")
except Exception as e:
_LOGGER.warning(f"Failed to setup Aquarea device: {aquarea_device.name} ({e})", exc_info=e)
_LOGGER.error(f"Failed to setup Aquarea device: {aquarea_device.name} ({e})", exc_info=e)
except AuthenticationError as e:
_LOGGER.error(f"Aquarea authentication failed (2FA may be required): {e}", exc_info=e)
_LOGGER.error("The Aquarea API currently does not support 2FA/MFA verification")
_LOGGER.error("Please check your Panasonic account settings and disable 2FA if needed")
except Exception as e:
_LOGGER.warning(f"Failed to setup Aquarea: {e}", exc_info=e)
_LOGGER.error(f"Failed to setup Aquarea: {e}", exc_info=e)


hass.data[DOMAIN][DATA_COORDINATORS] = data_coordinators
Expand Down
8 changes: 7 additions & 1 deletion custom_components/panasonic_cc/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@ class PanasonicButtonEntityDescription(ButtonEntityDescription):
func: Callable[[PanasonicDeviceCoordinator], Awaitable[Any]] | None = None


async def _update_app_version(coordinator: PanasonicDeviceCoordinator) -> None:
"""Update app version."""
app_version = getattr(coordinator.api_client, '_app_version', None)
if app_version and hasattr(app_version, 'refresh'):
await app_version.refresh()

APP_VERSION_DESCRIPTION = PanasonicButtonEntityDescription(
key="update_app_version",
name="Fetch latest app version",
icon="mdi:refresh",
entity_category=EntityCategory.DIAGNOSTIC,
func = lambda coordinator: coordinator.api_client.update_app_version()
func = _update_app_version
)

UPDATE_DATA_DESCRIPTION = ButtonEntityDescription(
Expand Down
2 changes: 1 addition & 1 deletion custom_components/panasonic_cc/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
PANASONIC_DEVICES = "panasonic_devices"
DATA_COORDINATORS = "data_coordinators"
ENERGY_COORDINATORS = "energy_coordinators"
AQUAREA_COORDINATORS = "aquarea_coorinators"
AQUAREA_COORDINATORS = "aquarea_coordinators"

COMPONENT_TYPES = [
Platform.CLIMATE,
Expand Down
22 changes: 17 additions & 5 deletions custom_components/panasonic_cc/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,15 @@ def device_id(self) -> str:

@property
def device_info(self)->DeviceInfo:
# Access app_version via private attribute
app_version = getattr(self._api_client, '_app_version', None)
version = app_version.version if app_version and hasattr(app_version, 'version') else '1.0.0'
return DeviceInfo(
identifiers={(DOMAIN, self._panasonic_device_info.id )},
manufacturer=MANUFACTURER,
model=self._panasonic_device_info.model,
name=self._panasonic_device_info.name,
sw_version=self._api_client.app_version
sw_version=version
)

def get_change_request_builder(self):
Expand Down Expand Up @@ -126,12 +129,15 @@ def energy(self) -> PanasonicDeviceEnergy | None:

@property
def device_info(self)->DeviceInfo:
# Access app_version via private attribute
app_version = getattr(self._api_client, '_app_version', None)
version = app_version.version if app_version and hasattr(app_version, 'version') else '1.0.0'
return DeviceInfo(
identifiers={(DOMAIN, self._panasonic_device_info.id )},
manufacturer=MANUFACTURER,
model=self._panasonic_device_info.model,
name=self._panasonic_device_info.name,
sw_version=self._api_client.app_version
sw_version=version
)

async def _fetch_device_data(self)->int:
Expand Down Expand Up @@ -165,7 +171,13 @@ def __init__(self, hass: HomeAssistant, config: dict, api_client: AquareaApiClie
self._aquarea_device_info = device_info
self._device:AquareaDevice | None = None
self._update_id = 0
self._is_demo = api_client._environment == AquareaEnvironment.DEMO
# Access environment via private attribute (_environment) from client
self._is_demo = self._get_environment() == AquareaEnvironment.DEMO

def _get_environment(self) -> AquareaEnvironment:
"""Get environment from client."""
# Access private attribute _environment to avoid modifying vendor library
return getattr(self._api_client, '_environment', AquareaEnvironment.PRODUCTION)

@property
def device(self) -> AquareaDevice:
Expand All @@ -188,8 +200,8 @@ def device_info(self)->DeviceInfo:
identifiers={(DOMAIN, self.device_id)},
manufacturer=self.device.manufacturer,
model="",
name=self.device.name,
sw_version=self.device.version,
name=self.device.device_name,
sw_version=self.device.firmware_version,
)

async def _fetch_device_data(self)->int:
Expand Down
2 changes: 1 addition & 1 deletion custom_components/panasonic_cc/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/sockless-coding/panasonic_cc/issues",
"requirements": ["aiohttp","aio-panasonic-comfort-cloud==2025.5.1","aioaquarea==0.7.2"],
"requirements": ["aiohttp","aio-panasonic-comfort-cloud==2025.5.1","aioaquarea==1.0.0"],
"quality_scale": "silver"
}
113 changes: 108 additions & 5 deletions custom_components/panasonic_cc/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
from typing import Callable
from dataclasses import dataclass

from homeassistant.const import PERCENTAGE
from homeassistant.const import PERCENTAGE, UnitOfTemperature

_LOGGER = logging.getLogger(__name__)
from homeassistant.core import HomeAssistant
from homeassistant.components.number import (
NumberDeviceClass,
Expand All @@ -12,18 +14,25 @@
)

from aio_panasonic_comfort_cloud import PanasonicDevice, PanasonicDeviceZone, ChangeRequestBuilder
from aioaquarea import Device as AquareaDevice
from aioaquarea import ExtendedOperationMode

from . import DOMAIN
from .const import DATA_COORDINATORS
from .coordinator import PanasonicDeviceCoordinator
from .base import PanasonicDataEntity
from .const import DATA_COORDINATORS, AQUAREA_COORDINATORS
from .coordinator import PanasonicDeviceCoordinator, AquareaDeviceCoordinator
from .base import PanasonicDataEntity, AquareaDataEntity

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

@dataclass(frozen=True, kw_only=True)
class AquareaNumberEntityDescription(NumberEntityDescription):
"""Describes Aquarea Number entity."""
zone_id: int

def create_zone_damper_description(zone: PanasonicDeviceZone):
return PanasonicNumberEntityDescription(
key = f"zone-{zone.id}-damper",
Expand All @@ -49,9 +58,51 @@ async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities):
data_coordinator,
create_zone_damper_description(zone)))

# Aquarea Number entities for temperature targets
aquarea_coordinators: list[AquareaDeviceCoordinator] = hass.data[DOMAIN][AQUAREA_COORDINATORS]
for coordinator in aquarea_coordinators:
for zone_id in coordinator.device.zones:
zone = coordinator.device.zones.get(zone_id)
# Add heat target temperature
devices.append(AquareaNumberEntity(
coordinator,
AquareaNumberEntityDescription(
zone_id=zone_id,
key=f"zone-{zone_id}-heat-target",
translation_key=f"zone-{zone_id}-heat-target",
name=f"{zone.name} Heat Target",
icon="mdi:thermometer",
device_class=NumberDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
native_max_value=zone.heat_max if zone.heat_max is not None else 30,
native_min_value=zone.heat_min if zone.heat_min is not None else 10,
native_step=1,
mode=NumberMode.BOX,
)
))
# Add cool target temperature if supported
if zone.cool_max and zone.cool_min:
devices.append(AquareaNumberEntity(
coordinator,
AquareaNumberEntityDescription(
zone_id=zone_id,
key=f"zone-{zone_id}-cool-target",
translation_key=f"zone-{zone_id}-cool-target",
name=f"{zone.name} Cool Target",
icon="mdi:thermometer",
device_class=NumberDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
native_max_value=zone.cool_max if zone.cool_max is not None else 30,
native_min_value=zone.cool_min if zone.cool_min is not None else 16,
native_step=1,
mode=NumberMode.BOX,
)
))

async_add_entities(devices)

class PanasonicNumberEntity(PanasonicDataEntity, NumberEntity):
"""Representation of a Panasonic Number."""

entity_description: PanasonicNumberEntityDescription

Expand All @@ -70,4 +121,56 @@ async def async_set_native_value(self, value: float) -> None:
self.async_write_ha_state()

def _async_update_attrs(self) -> None:
self._attr_native_value = self.entity_description.get_value(self.coordinator.device)
self._attr_native_value = self.entity_description.get_value(self.coordinator.device)


class AquareaNumberEntity(AquareaDataEntity, NumberEntity):
"""Aquarea Number entity for setting target temperatures."""

entity_description: AquareaNumberEntityDescription

def __init__(self, coordinator: AquareaDeviceCoordinator, description: AquareaNumberEntityDescription):
"""Initialize the number entity."""
self.entity_description = description
super().__init__(coordinator, description.key)

async def async_set_native_value(self, value: float) -> None:
"""Set new target temperature value."""
zone_id = self.entity_description.zone_id
temperature = int(value)

# Determine if we're setting heat or cool based on key
if "heat-target" in self.entity_description.key:
# Temporarily switch mode to HEAT if needed
original_mode = self.coordinator.device.mode
if original_mode not in (ExtendedOperationMode.HEAT, ExtendedOperationMode.AUTO_HEAT):
_LOGGER.debug(f"Switching to HEAT mode to set heat target for zone {zone_id}")
await self.coordinator.device.set_temperature(temperature, zone_id)
elif "cool-target" in self.entity_description.key:
# Temporarily switch mode to COOL if needed
original_mode = self.coordinator.device.mode
if original_mode not in (ExtendedOperationMode.COOL, ExtendedOperationMode.AUTO_COOL):
_LOGGER.debug(f"Switching to COOL mode to set cool target for zone {zone_id}")
await self.coordinator.device.set_temperature(temperature, zone_id)

self._attr_native_value = temperature
self.async_write_ha_state()
await self.coordinator.async_request_refresh()

def _async_update_attrs(self) -> None:
"""Update the current value."""
zone = self.coordinator.device.zones.get(self.entity_description.zone_id)
if zone:
# When heatSet/coolSet is not available from API, display current temperature as reference
if "heat-target" in self.entity_description.key:
# Try to get target temperature first, fallback to current temperature
temp = zone.heat_target_temperature
if temp is None:
# API doesn't return setpoint, use current temperature as reference
temp = zone.temperature
self._attr_native_value = temp
elif "cool-target" in self.entity_description.key:
temp = zone.cool_target_temperature
if temp is None:
temp = zone.temperature
self._attr_native_value = temp
Loading