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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ This is a custom integration to control Panasonic Comfort Cloud devices in [Home
### Water Heater
- **Aquarea Hot Water Tank** — Water heater entity with target temperature control (40–65°C), operation modes (Heat Pump, Off)

### Fan
- **Fan** — A dedicated fan entity for Panasonic indoor units, exposing fan speed as a percentage and as preset modes (mirroring the climate entity's fan_mode). Use it in dashboards or automations that cannot reach the climate entity's fan mode.

### Swing Control
- Horizontal swing mode via Select entity
- Vertical swing mode via Select entity
Expand Down
1 change: 1 addition & 0 deletions custom_components/panasonic_cc/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
# Platforms
COMPONENT_TYPES = [
Platform.CLIMATE,
Platform.FAN,
Platform.SENSOR,
Platform.SWITCH,
Platform.BUTTON,
Expand Down
18 changes: 18 additions & 0 deletions custom_components/panasonic_cc/fan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Fan entities for Panasonic and Aquarea devices."""
from __future__ import annotations

from typing import Any

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

from .panasonic.fan import async_setup_entry as panasonic_setup


async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: Any,
) -> None:
"""Set up the fan entities."""
await panasonic_setup(hass, entry, async_add_entities)
1 change: 1 addition & 0 deletions custom_components/panasonic_cc/panasonic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
# Platforms that the Panasonic slice provides
PLATFORMS = [
"climate",
"fan",
"sensor",
"switch",
"button",
Expand Down
198 changes: 198 additions & 0 deletions custom_components/panasonic_cc/panasonic/fan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
"""Panasonic fan entities."""
from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import Any

from homeassistant.components.fan import (
FanEntity,
FanEntityDescription,
)
from homeassistant.components.fan.const import FanEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant

from aio_panasonic_comfort_cloud import ChangeRequestBuilder, constants

from ..const import DOMAIN
from .base import PanasonicDataEntity
from .coordinator import PanasonicDeviceCoordinator
from .const import DATA_COORDINATORS

_LOGGER = logging.getLogger(__name__)

_FAN_SPEEDS = [f for f in constants.FanSpeed]
_SPEED_COUNT = len(_FAN_SPEEDS)


def _percentage_for_speed(speed: constants.FanSpeed) -> int:
"""Return the percentage value for a given fan speed."""
if speed not in _FAN_SPEEDS:
return 0
idx = _FAN_SPEEDS.index(speed)
return round((idx + 1) * 100 / _SPEED_COUNT)


def _speed_for_percentage(pct: int) -> constants.FanSpeed:
"""Return the nearest fan speed for a given percentage."""
if pct <= 0:
return _FAN_SPEEDS[0]
idx = round(pct / 100 * _SPEED_COUNT) - 1
idx = max(0, min(_SPEED_COUNT - 1, idx))
return _FAN_SPEEDS[idx]


@dataclass(frozen=True, kw_only=True)
class PanasonicFanEntityDescription(FanEntityDescription):
"""Describes a Panasonic fan entity."""


PANASONIC_FAN_DESCRIPTION = PanasonicFanEntityDescription(
key="fan",
translation_key="fan",
)


class PanasonicFanEntity(PanasonicDataEntity, FanEntity):
"""Representation of a Panasonic fan entity."""

entity_description: PanasonicFanEntityDescription

_attr_supported_features = (
FanEntityFeature.SET_SPEED
| FanEntityFeature.PRESET_MODE
| FanEntityFeature.TURN_ON
| FanEntityFeature.TURN_OFF
)
_attr_preset_modes = [f.name for f in _FAN_SPEEDS]
_attr_speed_count = _SPEED_COUNT

def __init__(
self,
coordinator: PanasonicDeviceCoordinator,
description: PanasonicFanEntityDescription,
) -> None:
"""Initialize the fan entity."""
self.entity_description = description
super().__init__(coordinator, description.key)

def _async_update_attrs(self) -> None:
"""Update attributes of the fan entity."""
parameters = self.coordinator.device.parameters
self._attr_is_on = parameters.power != constants.Power.Off
fan_speed = parameters.fan_speed
self._attr_percentage = _percentage_for_speed(fan_speed)
self._attr_preset_mode = fan_speed.name

@property
def percentage(self) -> int | None:
"""Return the current percentage."""
return self._attr_percentage

@property
def preset_mode(self) -> str | None:
"""Return the current preset mode."""
return self._attr_preset_mode

@property
def is_on(self) -> bool:
"""Return true if entity is on."""
return self._attr_is_on

async def async_set_percentage(self, percentage: int) -> None:
"""Set the fan speed by percentage."""
try:
speed = _speed_for_percentage(percentage)
builder = self.coordinator.get_change_request_builder()
builder.set_fan_speed(speed.name)
await self.coordinator.async_apply_changes(builder)
await self.coordinator.async_schedule_refresh()
self._attr_percentage = _percentage_for_speed(speed)
self._attr_preset_mode = speed.name
self.async_write_ha_state()
except Exception:
_LOGGER.exception(
"Failed to set fan percentage %s on device %s",
percentage,
self.coordinator.device_id,
)

async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the fan speed by preset mode."""
if self.preset_modes is None or preset_mode not in self.preset_modes:
raise ValueError(f"Unsupported preset_mode '{preset_mode}'")

try:
builder = self.coordinator.get_change_request_builder()
builder.set_fan_speed(preset_mode)
await self.coordinator.async_apply_changes(builder)
await self.coordinator.async_schedule_refresh()
self._attr_preset_mode = preset_mode
speed = next(
(f for f in _FAN_SPEEDS if f.name == preset_mode), None
)
if speed is not None:
self._attr_percentage = _percentage_for_speed(speed)
self.async_write_ha_state()
except Exception:
_LOGGER.exception(
"Failed to set fan preset mode %s on device %s",
preset_mode,
self.coordinator.device_id,
)

async def async_turn_on(
self,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs: Any,
) -> None:
"""Turn on the fan."""
try:
builder = self.coordinator.get_change_request_builder()
builder.set_power_mode(constants.Power.On)
if percentage is not None:
speed = _speed_for_percentage(percentage)
builder.set_fan_speed(speed.name)
elif preset_mode is not None:
builder.set_fan_speed(preset_mode)
await self.coordinator.async_apply_changes(builder)
await self.coordinator.async_schedule_refresh()
self._attr_is_on = True
self.async_write_ha_state()
except Exception:
_LOGGER.exception(
"Failed to turn on fan on device %s",
self.coordinator.device_id,
)

async def async_turn_off(self) -> None:
"""Turn off the fan."""
try:
builder = self.coordinator.get_change_request_builder()
builder.set_power_mode(constants.Power.Off)
await self.coordinator.async_apply_changes(builder)
await self.coordinator.async_schedule_refresh()
self._attr_is_on = False
self.async_write_ha_state()
except Exception:
_LOGGER.exception(
"Failed to turn off fan on device %s",
self.coordinator.device_id,
)


async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: Any,
) -> None:
"""Set up the Panasonic fan entities."""
entities = []
data_coordinators: list[PanasonicDeviceCoordinator] = hass.data[DOMAIN][
DATA_COORDINATORS
]
for coordinator in data_coordinators:
entities.append(PanasonicFanEntity(coordinator, PANASONIC_FAN_DESCRIPTION))
async_add_entities(entities)
5 changes: 5 additions & 0 deletions custom_components/panasonic_cc/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@
}
}
},
"fan": {
"fan": {
"name": "Fan"
}
},
"select": {
"horizontal_swing":{
"state": {
Expand Down
5 changes: 5 additions & 0 deletions custom_components/panasonic_cc/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@
}
}
},
"fan": {
"fan": {
"name": "Fan"
}
},
"select": {
"horizontal_swing":{
"state": {
Expand Down