├── .gitattributes
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug.yaml
│ └── feature_request.yaml
└── workflows
│ ├── hacs_validation.yml
│ ├── hassfest.yaml
│ └── stale.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── custom_components
└── panasonic_cc
│ ├── __init__.py
│ ├── base.py
│ ├── button.py
│ ├── climate.py
│ ├── config_flow.py
│ ├── const.py
│ ├── coordinator.py
│ ├── icons.json
│ ├── manifest.json
│ ├── number.py
│ ├── select.py
│ ├── sensor.py
│ ├── services.yaml
│ ├── strings.json
│ ├── switch.py
│ ├── translations
│ ├── cs.json
│ ├── de.json
│ ├── en.json
│ ├── it.json
│ ├── nb.json
│ ├── pl.json
│ └── se.json
│ └── water_heater.py
├── doc
├── configuration.png
├── controls.png
├── diagnostics.png
├── entities.png
├── options_dlg.png
├── sensors.png
├── setup.png
└── setup_dlg.png
├── hacs.json
├── info.md
└── requirements.txt
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [sockless-coding]
4 | #patreon: # Replace with a single Patreon username
5 | #open_collective: # Replace with a single Open Collective username
6 | #ko_fi: # Replace with a single Ko-fi username
7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | #liberapay: # Replace with a single Liberapay username
10 | #issuehunt: # Replace with a single IssueHunt username
11 | #lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | #polar: # Replace with a single Polar username
13 | buy_me_a_coffee: sockless
14 | #thanks_dev: # Replace with a single thanks.dev username
15 | #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: "Report an issue with Panasonic Comfort Cloud"
3 | description: "Report an issue with Panasonic Comfort Cloud"
4 | labels: "bug"
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: Before you open a new issue, search through the existing issues to see if others have had the same problem.
9 | - type: markdown
10 | attributes:
11 | value: |
12 | ## Environment
13 | - type: checkboxes
14 | attributes:
15 | label: Checklist
16 | options:
17 | - label: I have verified that the account and device works in the Comfort Cloud App.
18 | required: true
19 | - label: This issue only contains 1 issue (if you have multiple issues, open one issue for each issue).
20 | required: true
21 | - label: This issue is not a duplicate issue of currently [previous issues](https://github.com/sockless-coding/panasonic_cc/issues?q=is%3Aissue+label%3A"bug"+)..
22 | required: true
23 | - type: input
24 | id: version
25 | validations:
26 | required: true
27 | attributes:
28 | label: What version of Home Assistant Core?
29 | placeholder: core-
30 | description: >
31 | Can be found in: [Settings ⇒ System ⇒ Repairs ⇒ Three Dots in Upper Right ⇒ System information](https://my.home-assistant.io/redirect/system_health/).
32 |
33 | [](https://my.home-assistant.io/redirect/system_health/)
34 | - type: textarea
35 | attributes:
36 | label: "Describe the issue"
37 | description: "A clear and concise description of what the issue is."
38 | validations:
39 | required: true
40 | - type: textarea
41 | attributes:
42 | label: "Error/Debug Logs"
43 | description: "If applicable, add error or debug logs to help explain your problem."
44 | render: text
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yaml:
--------------------------------------------------------------------------------
1 | name: Feature Request
2 | description: Template for submitting feature requests for Panasonic Comfort Cloud
3 | labels: ['feature request', 'enhancement']
4 |
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | ## Feature Request
10 |
11 | **Is your feature request related to a problem? Please describe.**
12 | A clear and concise description of what the problem is.
13 |
14 | - type: textarea
15 | id: problem_description
16 | attributes:
17 | label: Problem Description
18 | description: Describe the problem your feature request is related to.
19 | placeholder: Describe the problem...
20 |
21 | - type: markdown
22 | attributes:
23 | value: |
24 | **Describe the solution you'd like**
25 | A clear and concise description of what you want to happen.
26 |
27 | - type: textarea
28 | id: solution_description
29 | attributes:
30 | label: Solution Description
31 | description: Describe the solution you'd like.
32 | placeholder: Describe the solution...
33 |
34 | - type: markdown
35 | attributes:
36 | value: |
37 | **Describe alternatives you've considered**
38 | A clear and concise description of any alternative solutions or features you've considered.
39 |
40 | - type: textarea
41 | id: alternatives_description
42 | attributes:
43 | label: Alternatives Description
44 | description: Describe any alternative solutions or features you've considered.
45 | placeholder: Describe the alternatives...
46 |
47 | - type: markdown
48 | attributes:
49 | value: |
50 | **Additional context**
51 | Add any other context or screenshots about the feature request here.
52 |
53 | - type: textarea
54 | id: additional_context
55 | attributes:
56 | label: Additional Context
57 | description: Add any other context or screenshots about the feature request.
58 | placeholder: Add any other context...
59 |
--------------------------------------------------------------------------------
/.github/workflows/hacs_validation.yml:
--------------------------------------------------------------------------------
1 | name: Validate
2 |
3 | on:
4 | push:
5 | pull_request:
6 | schedule:
7 | - cron: "0 0 * * *"
8 | workflow_dispatch:
9 |
10 | jobs:
11 | validate-hacs:
12 | runs-on: "ubuntu-latest"
13 | steps:
14 | - name: HACS validation
15 | uses: "hacs/action@main"
16 | with:
17 | category: "integration"
18 |
--------------------------------------------------------------------------------
/.github/workflows/hassfest.yaml:
--------------------------------------------------------------------------------
1 | name: Validate with hassfest
2 |
3 | on:
4 | push:
5 | pull_request:
6 | schedule:
7 | - cron: "0 0 * * *"
8 |
9 | jobs:
10 | validate:
11 | runs-on: "ubuntu-latest"
12 | steps:
13 | - uses: "actions/checkout@v3"
14 | - uses: home-assistant/actions/hassfest@master
15 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: Close Stale Issues
2 | on:
3 | schedule:
4 | - cron: "0 0 * * *" # Runs daily at midnight UTC
5 | jobs:
6 | stale:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/stale@v9.0.0
10 | with:
11 | repo-token: ${{ secrets.GITHUB_TOKEN }}
12 | days-before-stale: 60 # Number of days of inactivity before marking as stale
13 | days-before-close: 7 # Number of days to close after being marked as stale
14 | stale-issue-label: "stale"
15 | stale-issue-message: "Hi there! This issue has been marked as stale due to inactivity. If you believe this is still relevant, please let us know. Otherwise, it will be closed soon."
16 | close-issue-message: "This issue has been automatically closed due to inactivity. If you still need help, feel free to reopen or create a new issue. Thank you!"
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode/
2 | .idea/
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribution guidelines
2 |
3 | Contributing to this project should be as easy and transparent as possible, whether it's:
4 |
5 | - Reporting a bug
6 | - Discussing the current state of the code
7 | - Submitting a fix
8 | - Proposing new features
9 |
10 | ## Github is used for everything
11 |
12 | Github is used to host code, to track issues and feature requests, as well as accept pull requests.
13 |
14 | Pull requests are the best way to propose changes to the codebase.
15 |
16 | 1. Fork the repo and create your branch from `main`.
17 | 2. If you've changed something, update the documentation.
18 | 3. Make sure your code lints (using `scripts/lint`).
19 | 4. Test you contribution.
20 | 5. Issue that pull request!
21 |
22 | ## Any contributions you make will be under the MIT Software License
23 |
24 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
25 |
26 | ## Report bugs using Github's [issues](../../issues)
27 |
28 | GitHub issues are used to track public bugs.
29 | Report a bug by [opening a new issue](../../issues/new/choose); it's that easy!
30 |
31 | ## Write bug reports with detail, background, and sample code
32 |
33 | **Great Bug Reports** tend to have:
34 |
35 | - A quick summary and/or background
36 | - Steps to reproduce
37 | - Be specific!
38 | - Give sample code if you can.
39 | - What you expected would happen
40 | - What actually happens
41 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
42 |
43 | People *love* thorough bug reports. I'm not even kidding.
44 |
45 | ## Use a Consistent Coding Style
46 |
47 | Use [black](https://github.com/ambv/black) to make sure the code follows the style.
48 |
49 | ## Test your code modification
50 |
51 | This custom component is based on [integration_blueprint template](https://github.com/ludeeus/integration_blueprint).
52 |
53 | It comes with development environment in a container, easy to launch
54 | if you use Visual Studio Code. With this container you will have a stand alone
55 | Home Assistant instance running and already configured with the included
56 | [`configuration.yaml`](./config/configuration.yaml)
57 | file.
58 |
59 | ## License
60 |
61 | By contributing, you agree that your contributions will be licensed under its MIT License.
62 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-2024 Jimmy Everling
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Panasonic Comfort Cloud - HomeAssistant Component
2 |
3 | [![GitHub Release][releases-shield]][releases]
4 | [![License][license-shield]](LICENSE)
5 | [](https://github.com/hacs/integration)
6 | [](https://analytics.home-assistant.io/)
7 |
8 | This is a custom component to allow control of Panasonic Comfort Cloud devices in [HomeAssistant](https://home-assistant.io).
9 |
10 | > [!IMPORTANT]
11 | > Before installing this integration, please ensure the following steps have been completed in the Panasonic Comfort Cloud App:
12 | >
13 | > - **Set Up Two-Factor Authentication (2FA):** Complete the entire 2FA setup process.
14 | > - **Select the SMS Option:** It is crucial to choose the SMS option for 2FA. Failing to do so will result in the error “Missing required parameter: code.”
15 | >
16 | > For optimal operation, it is also recommended that you use separate accounts for Home Assistant and the Comfort Cloud App.
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | ## Features
27 |
28 | * Climate component for Panasonic airconditioners and heatpumps
29 | * Horizontal swing mode selection
30 | * Sensors for inside and outside temperature (where available)
31 | * Switch for toggling Nanoe mode (where available)
32 | * Switch for toggling ECONAVI mode (where available)
33 | * Switch for toggling AI ECO mode (where available)
34 | * Daily energy sensor (optional)
35 | * Current Power sensor (Calculated from energy reading)
36 | * Zone controls (where available)
37 |
38 | ## Installation
39 |
40 | ### HACS (recommended)
41 | 1. [Install HACS](https://hacs.xyz/docs/setup/download), if you did not already
42 | 2. [](https://my.home-assistant.io/redirect/hacs_repository/?owner=sockless-coding&repository=panasonic_cc&category=integration)
43 | 3. Press the Download button
44 | 4. Restart Home Assistant
45 | 5. [](https://my.home-assistant.io/redirect/config_flow_start/?domain=panasonic_cc)
46 |
47 | ### Install manually
48 | Clone or copy this repository and copy the folder 'custom_components/panasonic_cc' into '/custom_components/panasonic_cc'
49 |
50 | ## Configuration
51 |
52 | Once installed, the Panasonic Comfort Cloud integration can be configured via the Home Assistant integration interface where it will let you enter your Panasonic ID and Password.
53 |
54 | 
55 |
56 | After inital setup, the following options are available:
57 |
58 | 
59 |
60 | ## Known issues
61 |
62 | - The authentication process can be fiddly and may require resetting the MFA by logging in / out from the Panasonic app.
63 |
64 | ## Dependencies
65 |
66 | This integration uses the following modules:
67 |
68 | - [`aio-panasonic-comfort-cloud`](https://github.com/sockless-coding/aio-panasonic-comfort-cloud): For Panasonic Heatpumps.
69 | - [`aioaquarea`](https://github.com/cjaliaga/aioaquarea): For Panasonic Aquarea devices.
70 |
71 |
72 |
73 |
74 | ## Support Development
75 | - :coffee: [Buy me a coffee](https://www.buymeacoffee.com/sockless)
76 |
77 | [license-shield]: https://img.shields.io/github/license/sockless-coding/panasonic_cc.svg?style=for-the-badge
78 | [releases-shield]: https://img.shields.io/github/release/sockless-coding/panasonic_cc.svg?style=for-the-badge
79 | [releases]: https://github.com/sockless-coding/panasonic_cc/releases
80 |
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/__init__.py:
--------------------------------------------------------------------------------
1 | """Platform for the Panasonic Comfort Cloud."""
2 | import logging
3 | from typing import Dict
4 |
5 | import asyncio
6 |
7 | import voluptuous as vol
8 |
9 | from homeassistant.core import HomeAssistant
10 | from homeassistant.config_entries import ConfigEntry
11 | from homeassistant.const import (
12 | CONF_USERNAME, CONF_PASSWORD)
13 | import homeassistant.helpers.config_validation as cv
14 | from homeassistant.helpers.aiohttp_client import async_get_clientsession
15 | from homeassistant.loader import async_get_integration
16 | from aio_panasonic_comfort_cloud import ApiClient
17 | from aioaquarea import Client as AquareaApiClient, AquareaEnvironment
18 |
19 | from .const import (
20 | CONF_UPDATE_INTERVAL_VERSION,
21 | CONF_ENABLE_DAILY_ENERGY_SENSOR,
22 | CONF_DEVICE_FETCH_INTERVAL,
23 | CONF_ENERGY_FETCH_INTERVAL,
24 | DEFAULT_DEVICE_FETCH_INTERVAL,
25 | DEFAULT_ENERGY_FETCH_INTERVAL,
26 | DEFAULT_ENABLE_DAILY_ENERGY_SENSOR,
27 | CONF_USE_PANASONIC_PRESET_NAMES,
28 | PANASONIC_DEVICES,
29 | COMPONENT_TYPES,
30 | STARTUP,
31 | DATA_COORDINATORS,
32 | ENERGY_COORDINATORS,
33 | AQUAREA_COORDINATORS)
34 |
35 | from .coordinator import PanasonicDeviceCoordinator, PanasonicDeviceEnergyCoordinator, AquareaDeviceCoordinator
36 |
37 |
38 | _LOGGER = logging.getLogger(__name__)
39 |
40 | DOMAIN = "panasonic_cc"
41 |
42 | CONFIG_SCHEMA = vol.Schema(
43 | {
44 | DOMAIN: vol.Schema(
45 | {
46 | vol.Required(CONF_USERNAME): cv.string,
47 | vol.Required(CONF_PASSWORD): cv.string,
48 | vol.Optional(CONF_ENABLE_DAILY_ENERGY_SENSOR, default=DEFAULT_ENABLE_DAILY_ENERGY_SENSOR): cv.boolean,
49 | # noqa: E501
50 | }
51 | )
52 | },
53 | extra=vol.ALLOW_EXTRA,
54 | )
55 |
56 | AQUAREA_DEMO = False
57 |
58 | def setup(hass, config):
59 | pass
60 |
61 |
62 | async def async_setup(hass: HomeAssistant, config: Dict) -> bool:
63 | """Set up the Garo Wallbox component."""
64 |
65 | hass.data.setdefault(DOMAIN, {})
66 | integration = await async_get_integration(hass, DOMAIN)
67 | _LOGGER.info(STARTUP, integration.version)
68 | return True
69 |
70 |
71 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
72 | """Establish connection with Comfort Cloud."""
73 |
74 |
75 | conf = entry.data
76 | if PANASONIC_DEVICES not in hass.data:
77 | hass.data[PANASONIC_DEVICES] = []
78 |
79 | username = conf[CONF_USERNAME]
80 | password = conf[CONF_PASSWORD]
81 | enable_daily_energy_sensor = entry.options.get(CONF_ENABLE_DAILY_ENERGY_SENSOR, DEFAULT_ENABLE_DAILY_ENERGY_SENSOR)
82 |
83 | client = async_get_clientsession(hass)
84 | api = ApiClient(username, password, client)
85 | await api.start_session()
86 | devices = api.get_devices()
87 |
88 | if CONF_UPDATE_INTERVAL_VERSION not in conf or conf[CONF_UPDATE_INTERVAL_VERSION] < 2:
89 | _LOGGER.info("Updating configuration")
90 | updated_config = dict(entry.data)
91 | updated_config[CONF_UPDATE_INTERVAL_VERSION] = 2
92 | if CONF_DEVICE_FETCH_INTERVAL not in conf or conf[CONF_DEVICE_FETCH_INTERVAL] <= 31:
93 | updated_config[CONF_DEVICE_FETCH_INTERVAL] = DEFAULT_DEVICE_FETCH_INTERVAL
94 | _LOGGER.info(f"Setting default fetch interval to {DEFAULT_DEVICE_FETCH_INTERVAL}")
95 | if CONF_ENERGY_FETCH_INTERVAL not in conf or conf[CONF_ENERGY_FETCH_INTERVAL] <= 61:
96 | updated_config[CONF_ENERGY_FETCH_INTERVAL] = DEFAULT_ENERGY_FETCH_INTERVAL
97 | _LOGGER.info(f"Setting default energy fetch interval to {DEFAULT_ENERGY_FETCH_INTERVAL}")
98 | hass.config_entries.async_update_entry(entry, data=updated_config)
99 |
100 |
101 | if len(devices) == 0 and not api.has_unknown_devices:
102 | _LOGGER.error("Could not find any Panasonic Comfort Cloud Heat Pumps")
103 | return False
104 |
105 | _LOGGER.info("Got %s devices", len(devices))
106 | data_coordinators: list[PanasonicDeviceCoordinator] = []
107 | energy_coordinators: list[PanasonicDeviceEnergyCoordinator] = []
108 | aquarea_coordinators: list[AquareaDeviceCoordinator] = []
109 |
110 |
111 | for device in devices:
112 | try:
113 | device_coordinator = PanasonicDeviceCoordinator(hass, conf, api, device)
114 | await device_coordinator.async_config_entry_first_refresh()
115 | data_coordinators.append(device_coordinator)
116 | if enable_daily_energy_sensor:
117 | energy_coordinators.append(PanasonicDeviceEnergyCoordinator(hass, conf, api, device))
118 | except Exception as e:
119 | _LOGGER.warning(f"Failed to setup device: {device.name} ({e})", exc_info=e)
120 |
121 | if api.has_unknown_devices or AQUAREA_DEMO:
122 | try:
123 |
124 | if not AQUAREA_DEMO:
125 | aquarea_api_client = AquareaApiClient(client, username, password)
126 | await aquarea_api_client.login()
127 | else:
128 | 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)
132 | for aquarea_device in aquarea_devices:
133 | try:
134 | aquarea_device_coordinator = AquareaDeviceCoordinator(hass, conf, aquarea_api_client, aquarea_device)
135 | await aquarea_device_coordinator.async_config_entry_first_refresh()
136 | aquarea_coordinators.append(aquarea_device_coordinator)
137 | except Exception as e:
138 | _LOGGER.warning(f"Failed to setup Aquarea device: {aquarea_device.name} ({e})", exc_info=e)
139 | except Exception as e:
140 | _LOGGER.warning(f"Failed to setup Aquarea: {e}", exc_info=e)
141 |
142 |
143 | hass.data[DOMAIN][DATA_COORDINATORS] = data_coordinators
144 | hass.data[DOMAIN][ENERGY_COORDINATORS] = energy_coordinators
145 | hass.data[DOMAIN][AQUAREA_COORDINATORS] = aquarea_coordinators
146 | await asyncio.gather(
147 | *(
148 | data.async_config_entry_first_refresh()
149 | for data in energy_coordinators
150 | ),
151 | return_exceptions=True
152 | )
153 |
154 | await hass.config_entries.async_forward_entry_setups(entry, COMPONENT_TYPES)
155 | return True
156 |
157 |
158 | async def async_unload_entry(hass: HomeAssistant, entry):
159 | """Unload a config entry."""
160 | return await hass.config_entries.async_unload_platforms(entry, COMPONENT_TYPES)
161 |
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/base.py:
--------------------------------------------------------------------------------
1 | from abc import abstractmethod
2 |
3 |
4 | from homeassistant.helpers.update_coordinator import CoordinatorEntity
5 |
6 | from .coordinator import PanasonicDeviceCoordinator, PanasonicDeviceEnergyCoordinator, AquareaDeviceCoordinator
7 |
8 | class PanasonicDataEntity(CoordinatorEntity[PanasonicDeviceCoordinator]):
9 |
10 | _attr_has_entity_name = True
11 |
12 | def __init__(self, coordinator: PanasonicDeviceCoordinator, key: str) -> None:
13 | super().__init__(coordinator)
14 | self._attr_translation_key = key
15 | self._attr_unique_id = f"{coordinator.device_id}-{key}"
16 | self._attr_device_info = self.coordinator.device_info
17 | self._async_update_attrs()
18 |
19 |
20 | def _handle_coordinator_update(self) -> None:
21 | """Handle updated data from the coordinator."""
22 | self._async_update_attrs()
23 | self.async_write_ha_state()
24 |
25 | @abstractmethod
26 | def _async_update_attrs(self) -> None:
27 | """Update the attributes of the entity."""
28 |
29 | class PanasonicEnergyEntity(CoordinatorEntity[PanasonicDeviceEnergyCoordinator]):
30 |
31 | _attr_has_entity_name = True
32 |
33 | def __init__(self, coordinator: PanasonicDeviceEnergyCoordinator, key: str) -> None:
34 | super().__init__(coordinator)
35 | self._attr_translation_key = key
36 | self._attr_unique_id = f"{coordinator.device_id}-{key}"
37 | self._attr_device_info = self.coordinator.device_info
38 | self._async_update_attrs()
39 |
40 |
41 | def _handle_coordinator_update(self) -> None:
42 | """Handle updated data from the coordinator."""
43 | self._async_update_attrs()
44 | self.async_write_ha_state()
45 |
46 | @abstractmethod
47 | def _async_update_attrs(self) -> None:
48 | """Update the attributes of the entity."""
49 |
50 | class AquareaDataEntity(CoordinatorEntity[AquareaDeviceCoordinator]):
51 |
52 | _attr_has_entity_name = True
53 |
54 | def __init__(self, coordinator: AquareaDeviceCoordinator, key: str) -> None:
55 | super().__init__(coordinator)
56 | self._attr_translation_key = key
57 | self._attr_unique_id = f"{coordinator.device_id}-{key}"
58 | self._attr_device_info = self.coordinator.device_info
59 | self._async_update_attrs()
60 |
61 |
62 | def _handle_coordinator_update(self) -> None:
63 | """Handle updated data from the coordinator."""
64 | self._async_update_attrs()
65 | self.async_write_ha_state()
66 |
67 | @abstractmethod
68 | def _async_update_attrs(self) -> None:
69 | """Update the attributes of the entity."""
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/button.py:
--------------------------------------------------------------------------------
1 | from typing import Callable, Awaitable, Any
2 | from dataclasses import dataclass
3 | import logging
4 |
5 | from homeassistant.core import HomeAssistant
6 | from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
7 | from homeassistant.const import EntityCategory
8 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
9 | from .const import DOMAIN, DATA_COORDINATORS, ENERGY_COORDINATORS
10 | from .coordinator import PanasonicDeviceCoordinator, PanasonicDeviceEnergyCoordinator
11 | from .base import PanasonicDataEntity
12 |
13 | _LOGGER = logging.getLogger(__name__)
14 |
15 | @dataclass(frozen=True, kw_only=True)
16 | class PanasonicButtonEntityDescription(ButtonEntityDescription):
17 | """Describes a Panasonic Button entity."""
18 | func: Callable[[PanasonicDeviceCoordinator], Awaitable[Any]] | None = None
19 |
20 |
21 | APP_VERSION_DESCRIPTION = PanasonicButtonEntityDescription(
22 | key="update_app_version",
23 | name="Fetch latest app version",
24 | icon="mdi:refresh",
25 | entity_category=EntityCategory.DIAGNOSTIC,
26 | func = lambda coordinator: coordinator.api_client.update_app_version()
27 | )
28 |
29 | UPDATE_DATA_DESCRIPTION = ButtonEntityDescription(
30 | key="update_data",
31 | name="Fetch latest data",
32 | icon="mdi:update",
33 | entity_category=EntityCategory.DIAGNOSTIC
34 | )
35 | UPDATE_ENERGY_DESCRIPTION = ButtonEntityDescription(
36 | key="update_energy",
37 | name="Fetch latest energy data",
38 | icon="mdi:update",
39 | entity_category=EntityCategory.DIAGNOSTIC
40 | )
41 |
42 | async def async_setup_entry(hass: HomeAssistant, config, async_add_entities):
43 | entities = []
44 | data_coordinators: list[PanasonicDeviceCoordinator] = hass.data[DOMAIN][DATA_COORDINATORS]
45 | energy_coordinators: list[PanasonicDeviceEnergyCoordinator] = hass.data[DOMAIN][ENERGY_COORDINATORS]
46 |
47 | for coordinator in data_coordinators:
48 | entities.append(PanasonicButtonEntity(coordinator, APP_VERSION_DESCRIPTION))
49 | entities.append(CoordinatorUpdateButtonEntity(coordinator, UPDATE_DATA_DESCRIPTION))
50 | for coordinator in energy_coordinators:
51 | entities.append(CoordinatorUpdateButtonEntity(coordinator, UPDATE_ENERGY_DESCRIPTION))
52 |
53 | async_add_entities(entities)
54 |
55 | class PanasonicButtonEntity(PanasonicDataEntity, ButtonEntity):
56 | """Representation of a Panasonic Button."""
57 |
58 | entity_description: PanasonicButtonEntityDescription
59 |
60 | def __init__(self, coordinator: PanasonicDeviceCoordinator, description: PanasonicButtonEntityDescription) -> None:
61 | self.entity_description = description
62 | super().__init__(coordinator, description.key)
63 |
64 |
65 | def _async_update_attrs(self) -> None:
66 | """Update the attributes of the entity."""
67 |
68 | async def async_press(self) -> None:
69 | """Press the button."""
70 | if self.entity_description.func:
71 | await self.entity_description.func(self.coordinator)
72 |
73 | class CoordinatorUpdateButtonEntity(PanasonicDataEntity, ButtonEntity):
74 | """Representation of a Coordinator Update Button."""
75 |
76 | def __init__(self, coordinator: DataUpdateCoordinator, description: ButtonEntityDescription) -> None:
77 | self.entity_description = description
78 | super().__init__(coordinator, description.key)
79 |
80 |
81 | def _async_update_attrs(self) -> None:
82 | """Update the attributes of the entity."""
83 |
84 | async def async_press(self) -> None:
85 | """Press the button."""
86 | await self.coordinator.async_request_refresh()
87 |
88 |
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/climate.py:
--------------------------------------------------------------------------------
1 | """Support for the Panasonic HVAC."""
2 | from typing import Callable, Any
3 | from dataclasses import dataclass
4 | import logging
5 |
6 | import voluptuous as vol
7 |
8 | from homeassistant.core import HomeAssistant
9 | from homeassistant.components.climate import ClimateEntity, ClimateEntityDescription, HVACAction, HVACMode, ATTR_HVAC_MODE
10 | from homeassistant.helpers import config_validation as cv, entity_platform
11 | from homeassistant.config_entries import ConfigEntry
12 | from homeassistant.const import UnitOfTemperature, ATTR_TEMPERATURE
13 | from homeassistant.components.climate.const import ClimateEntityFeature
14 |
15 |
16 | from .base import PanasonicDataEntity, AquareaDataEntity
17 | from .coordinator import PanasonicDeviceCoordinator, AquareaDeviceCoordinator
18 | from aio_panasonic_comfort_cloud import PanasonicDeviceParameters, ChangeRequestBuilder, constants
19 | from aioaquarea import (
20 | ExtendedOperationMode as AquareaExtendedOperationMode,
21 | OperationStatus as AquareaZoneOperationStatus,
22 | DeviceAction as AquareaDeviceAction,
23 | UpdateOperationMode as AquareaUpdateOperationMode
24 | )
25 |
26 | from .const import (
27 | SUPPORT_FLAGS,
28 | SERVICE_SET_SWING_LR_MODE,
29 | PRESET_8_15,
30 | PRESET_NONE,
31 | PRESET_ECO,
32 | PRESET_BOOST,
33 | PRESET_QUIET,
34 | PRESET_POWERFUL,
35 | DOMAIN,
36 | DATA_COORDINATORS,
37 | AQUAREA_COORDINATORS,
38 | CONF_USE_PANASONIC_PRESET_NAMES)
39 |
40 | _LOGGER = logging.getLogger(__name__)
41 |
42 | @dataclass(frozen=True, kw_only=True)
43 | class PanasonicClimateEntityDescription(ClimateEntityDescription):
44 | """Describes a Panasonic climate entity."""
45 |
46 | @dataclass(frozen=True, kw_only=True)
47 | class AquareaClimateEntityDescription(ClimateEntityDescription):
48 | """Describes a Aquarea climate entity."""
49 | zone_id:int
50 |
51 | PANASONIC_CLIMATE_DESCRIPTION = PanasonicClimateEntityDescription(
52 | key="climate",
53 | translation_key="climate",
54 | )
55 |
56 | def convert_operation_mode_to_hvac_mode(operation_mode: constants.OperationMode, iauto: bool) -> HVACMode | None:
57 | """Convert OperationMode to HVAC mode."""
58 | match operation_mode:
59 | case constants.OperationMode.Auto:
60 | return HVACMode.COOL if iauto else HVACMode.HEAT_COOL
61 | case constants.OperationMode.Cool:
62 | return HVACMode.COOL
63 | case constants.OperationMode.Dry:
64 | return HVACMode.DRY
65 | case constants.OperationMode.Fan:
66 | return HVACMode.FAN_ONLY
67 | case constants.OperationMode.Heat:
68 | return HVACMode.HEAT
69 |
70 | def convert_hvac_mode_to_operation_mode(hvac_mode: HVACMode) -> constants.OperationMode | None:
71 | """Convert HVAC mode to OperationMode."""
72 | match hvac_mode:
73 | case HVACMode.HEAT_COOL:
74 | return constants.OperationMode.Auto
75 | case HVACMode.COOL:
76 | return constants.OperationMode.Cool
77 | case HVACMode.DRY:
78 | return constants.OperationMode.Dry
79 | case HVACMode.FAN_ONLY:
80 | return constants.OperationMode.Fan
81 | case HVACMode.HEAT:
82 | return constants.OperationMode.Heat
83 |
84 | def convert_state_to_hvac_action(state: PanasonicDeviceParameters) -> HVACAction | None:
85 | """Convert state to HVAC action."""
86 | if state.power == constants.Power.Off:
87 | return HVACAction.OFF
88 |
89 | match state.mode:
90 | case constants.OperationMode.Auto:
91 | auto_diff = state.target_temperature - state.inside_temperature
92 | if auto_diff >= 1:
93 | return HVACAction.HEATING
94 | elif auto_diff <= -1:
95 | return HVACAction.COOLING
96 | return HVACAction.IDLE
97 | case constants.OperationMode.Cool:
98 | return HVACAction.COOLING if state.target_temperature < state.inside_temperature else HVACAction.IDLE
99 | case constants.OperationMode.Dry:
100 | return HVACAction.DRYING
101 | case constants.OperationMode.Fan:
102 | return HVACAction.FAN
103 | case constants.OperationMode.Heat:
104 | return HVACAction.HEATING if state.target_temperature > state.inside_temperature else HVACAction.IDLE
105 |
106 | def convert_mode_and_status_to_hvac_mode(
107 | mode: AquareaExtendedOperationMode, zone_status: AquareaZoneOperationStatus
108 | ) -> HVACMode:
109 | if zone_status == AquareaZoneOperationStatus.OFF:
110 | return HVACMode.OFF
111 | match mode:
112 | case AquareaExtendedOperationMode.HEAT:
113 | return HVACMode.HEAT
114 | case AquareaExtendedOperationMode.COOL:
115 | return HVACMode.COOL
116 | case AquareaExtendedOperationMode.AUTO_COOL:
117 | return HVACMode.HEAT_COOL
118 | case AquareaExtendedOperationMode.AUTO_HEAT:
119 | return HVACMode.HEAT_COOL
120 |
121 | return HVACMode.OFF
122 |
123 | def convert_aquarea_action_to_hvac_action(action: AquareaDeviceAction) -> HVACAction:
124 | """Convert device action to HVAC action."""
125 | match action:
126 | case AquareaDeviceAction.COOLING:
127 | return HVACAction.COOLING
128 | case AquareaDeviceAction.HEATING:
129 | return HVACAction.HEATING
130 | return HVACAction.IDLE
131 |
132 | def convert_hvac_mode_to_aquarea_operation_mode(mode: HVACMode) -> AquareaUpdateOperationMode:
133 | """Convert HVAC mode to update operation mode."""
134 | match mode:
135 | case HVACMode.HEAT:
136 | return AquareaUpdateOperationMode.HEAT
137 | case HVACMode.COOL:
138 | return AquareaUpdateOperationMode.COOL
139 | case HVACMode.HEAT_COOL:
140 | return AquareaUpdateOperationMode.AUTO
141 | return AquareaUpdateOperationMode.OFF
142 |
143 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities):
144 | entities = []
145 | data_coordinators: list[PanasonicDeviceCoordinator] = hass.data[DOMAIN][DATA_COORDINATORS]
146 | aquarea_coordinators: list[AquareaDeviceCoordinator] = hass.data[DOMAIN][AQUAREA_COORDINATORS]
147 | use_panasonic_preset_names = entry.options.get(CONF_USE_PANASONIC_PRESET_NAMES, False)
148 | for coordinator in data_coordinators:
149 | entities.append(PanasonicClimateEntity(coordinator, PANASONIC_CLIMATE_DESCRIPTION, use_panasonic_preset_names))
150 | for aquarea_coordinator in aquarea_coordinators:
151 | for zone_id in aquarea_coordinator.device.zones:
152 | entities.append(AquareaClimateEntity(
153 | aquarea_coordinator,
154 | AquareaClimateEntityDescription(
155 | zone_id=zone_id,
156 | name=aquarea_coordinator.device.zones.get(zone_id).name,
157 | key=f"zone-{zone_id}-climate",
158 | translation_key=f"zone-{zone_id}-climate"
159 | )))
160 | async_add_entities(entities)
161 |
162 | platform = entity_platform.current_platform.get()
163 |
164 | platform.async_register_entity_service(
165 | SERVICE_SET_SWING_LR_MODE,
166 | {
167 | vol.Required('swing_mode'): cv.string,
168 | },
169 | "async_set_horizontal_swing_mode",
170 | )
171 |
172 |
173 | class PanasonicClimateEntity(PanasonicDataEntity, ClimateEntity):
174 | """Representation of a Panasonic Climate Device."""
175 |
176 | _attr_temperature_unit = UnitOfTemperature.CELSIUS
177 | _attr_target_temperature_step = 0.5
178 | _attr_supported_features = SUPPORT_FLAGS
179 | _attr_fan_modes = [f.name for f in constants.FanSpeed]
180 | _attr_name = None
181 |
182 | def __init__(self, coordinator: PanasonicDeviceCoordinator, description: PanasonicClimateEntityDescription, use_panasonic_preset_names: bool):
183 | """Initialize the climate entity."""
184 | self.entity_description = description
185 | device = coordinator.device
186 | hvac_modes = [HVACMode.OFF]
187 | if device.features.auto_mode:
188 | hvac_modes += [HVACMode.HEAT_COOL]
189 | if device.features.cool_mode:
190 | hvac_modes += [HVACMode.COOL]
191 | if device.features.dry_mode:
192 | hvac_modes += [HVACMode.DRY]
193 | hvac_modes += [HVACMode.FAN_ONLY]
194 | if device.features.heat_mode:
195 | hvac_modes += [HVACMode.HEAT]
196 | self._attr_hvac_modes = hvac_modes
197 |
198 | self._quiet_preset = PRESET_QUIET if use_panasonic_preset_names else PRESET_ECO
199 | self._powerful_preset = PRESET_POWERFUL if use_panasonic_preset_names else PRESET_BOOST
200 |
201 | preset_modes = [PRESET_NONE]
202 | if device.features.quiet_mode:
203 | preset_modes += [self._quiet_preset]
204 | if device.features.powerful_mode:
205 | preset_modes += [self._powerful_preset]
206 | if device.features.summer_house > 0:
207 | preset_modes += [PRESET_8_15]
208 | self._attr_preset_modes = preset_modes
209 |
210 | self._attr_swing_modes = [opt.name for opt in constants.AirSwingUD if opt != constants.AirSwingUD.Swing or device.features.auto_swing_ud]
211 |
212 | if device.has_horizontal_swing:
213 | self._attr_supported_features |= ClimateEntityFeature.SWING_HORIZONTAL_MODE
214 | self._attr_swing_horizontal_modes = [opt.name for opt in constants.AirSwingLR if opt != constants.AirSwingLR.Unavailable]
215 |
216 | super().__init__(coordinator, description.key)
217 | _LOGGER.info(f"Registing Climate entity: '{self._attr_unique_id}'")
218 |
219 |
220 |
221 |
222 | def _async_update_attrs(self) -> None:
223 | """Update attributes."""
224 | state = self.coordinator.device.parameters
225 | self._attr_hvac_mode = (HVACMode.OFF
226 | if state.power == constants.Power.Off
227 | else convert_operation_mode_to_hvac_mode(
228 | state.mode,
229 | state.iautox_mode == constants.IAutoXMode.On))
230 |
231 |
232 | self._set_temp_range()
233 | self._attr_current_temperature = state.inside_temperature
234 | self._attr_target_temperature = state.target_temperature
235 | self._attr_fan_mode = state.fan_speed.name
236 | self._attr_swing_mode = state.vertical_swing_mode.name
237 | if self.coordinator.device.has_horizontal_swing:
238 | self._attr_swing_horizontal_mode = state.horizontal_swing_mode.name
239 |
240 | if self.coordinator.device.in_summer_house_mode:
241 | self._attr_preset_mode = PRESET_8_15
242 | elif state.eco_mode == constants.EcoMode.Quiet:
243 | self._attr_preset_mode = self._quiet_preset
244 | elif state.eco_mode == constants.EcoMode.Powerful:
245 | self._attr_preset_mode = self._powerful_preset
246 | else:
247 | self._attr_preset_mode = PRESET_NONE
248 | if self.coordinator.device.has_inside_temperature:
249 | self._attr_hvac_action = convert_state_to_hvac_action(state)
250 |
251 |
252 | def _set_temp_range(self) -> None:
253 | """Set new target temperature range."""
254 | device = self.coordinator.device
255 | self._attr_min_temp = 8 if device.in_summer_house_mode else 16
256 | if device.in_summer_house_mode:
257 | self._attr_max_temp = 15 if device.features.summer_house == 2 else 10
258 | else:
259 | self._attr_max_temp = 30
260 |
261 | def _update_attributes(self, builder: ChangeRequestBuilder) -> None:
262 | """Update attributes."""
263 | if builder.power_mode == constants.Power.Off:
264 | self._attr_hvac_mode = HVACMode.OFF
265 | default_preset = PRESET_NONE
266 | if builder.target_temperature:
267 | self._attr_target_temperature = builder.target_temperature
268 | if builder.target_temperature > 15 and self._attr_preset_mode == PRESET_8_15:
269 | self._attr_preset_mode = default_preset
270 | elif builder.target_temperature < 15 and self._attr_preset_mode != PRESET_8_15:
271 | self._attr_preset_mode = default_preset = PRESET_8_15
272 |
273 | if builder.eco_mode:
274 | if builder.eco_mode.name in (PRESET_QUIET, PRESET_ECO):
275 | self._attr_preset_mode = self._quiet_preset
276 | elif builder.eco_mode.name in (PRESET_POWERFUL, PRESET_BOOST):
277 | self._attr_preset_mode = self._powerful_preset
278 | else:
279 | self._attr_preset_mode = default_preset
280 |
281 | if builder.fan_speed:
282 | self._attr_fan_mode = builder.fan_speed.name
283 | if builder.vertical_swing:
284 | self._attr_swing_mode = builder.vertical_swing.name
285 | if builder.horizontal_swing:
286 | self._attr_swing_horizontal_mode = builder.horizontal_swing.name
287 | if builder.hvac_mode:
288 | self._attr_hvac_mode = convert_operation_mode_to_hvac_mode(builder.hvac_mode, False)
289 | self.async_write_ha_state()
290 |
291 |
292 | async def _async_enter_summer_house_mode(self, builder: ChangeRequestBuilder):
293 | """Enter summer house mode."""
294 | device = self.coordinator.device
295 | stored_data = await self.coordinator.async_get_stored_data()
296 |
297 | stored_data['mode'] = device.parameters.mode.value
298 | stored_data['ecoMode'] = device.parameters.eco_mode.value
299 | stored_data['targetTemperature'] = device.parameters.target_temperature
300 | stored_data['fanSpeed'] = device.parameters.fan_speed.value
301 | await self.coordinator.async_store_data(stored_data)
302 |
303 | builder.set_hvac_mode(constants.OperationMode.Heat)
304 | builder.set_eco_mode(constants.EcoMode.Powerful)
305 | builder.set_target_temperature(8)
306 | builder.set_fan_speed(constants.FanSpeed.High)
307 |
308 | self._attr_min_temp = 8
309 | self._attr_max_temp = 15 if device.features.summer_house == 2 else 10
310 |
311 | async def _async_exit_summer_house_mode(self, builder: ChangeRequestBuilder) -> Callable[[ClimateEntity], None]:
312 | """Exit summer house mode."""
313 | self._attr_min_temp = 16
314 | self._attr_max_temp = 30
315 | if not self.coordinator.device.in_summer_house_mode:
316 | return
317 | stored_data = await self.coordinator.async_get_stored_data()
318 | try:
319 | hvac_mode = constants.OperationMode(stored_data['mode']) if 'mode' in stored_data else constants.OperationMode.Heat
320 | except:
321 | hvac_mode = constants.OperationMode.Heat
322 | try:
323 | eco_mode = constants.EcoMode(stored_data['ecoMode']) if 'ecoMode' in stored_data else constants.EcoMode.Auto
324 | except:
325 | eco_mode = constants.EcoMode.Auto
326 | target_temperature = stored_data['targetTemperature'] if 'targetTemperature' in stored_data else 20
327 | try:
328 | fan_speed = constants.FanSpeed(stored_data['fanSpeed']) if 'fanSpeed' in stored_data else constants.FanSpeed.Auto
329 | except:
330 | fan_speed = constants.FanSpeed.Auto
331 |
332 | builder.set_hvac_mode(hvac_mode)
333 | builder.set_eco_mode(eco_mode)
334 | builder.set_target_temperature(target_temperature)
335 | builder.set_fan_speed(fan_speed)
336 |
337 | async def async_turn_on(self) -> None:
338 | """Set the climate state to on."""
339 | builder = self.coordinator.get_change_request_builder()
340 | builder.set_power_mode(constants.Power.On)
341 | await self.coordinator.async_apply_changes(builder)
342 | await self.coordinator.async_request_refresh()
343 | self.async_write_ha_state()
344 |
345 | async def async_turn_off(self) -> None:
346 | """Set the climate state to off."""
347 | builder = self.coordinator.get_change_request_builder()
348 | builder.set_power_mode(constants.Power.Off)
349 | await self.coordinator.async_apply_changes(builder)
350 | self._attr_hvac_mode = HVACMode.OFF
351 | self.async_write_ha_state()
352 |
353 | async def async_set_temperature(self, **kwargs: Any) -> None:
354 | """Set the climate temperature."""
355 | builder = self.coordinator.get_change_request_builder()
356 | if temp := kwargs.get(ATTR_TEMPERATURE):
357 | builder.set_target_temperature(temp)
358 | if mode := kwargs.get(ATTR_HVAC_MODE):
359 | if op_mode := convert_hvac_mode_to_operation_mode(mode):
360 | builder.set_hvac_mode(op_mode)
361 | else:
362 | mode = None
363 | await self.coordinator.async_apply_changes(builder)
364 | self._update_attributes(builder)
365 |
366 | async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
367 | """Set new target hvac mode."""
368 | if hvac_mode == HVACMode.OFF:
369 | await self.async_turn_off()
370 | return
371 | if not (op_mode := convert_hvac_mode_to_operation_mode(hvac_mode)):
372 | raise ValueError(f"Invalid hvac mode {hvac_mode}")
373 |
374 | builder = self.coordinator.get_change_request_builder()
375 | await self._async_exit_summer_house_mode(builder)
376 | builder.set_hvac_mode(op_mode)
377 | await self.coordinator.async_apply_changes(builder)
378 | self._update_attributes(builder)
379 |
380 | async def async_set_preset_mode(self, preset_mode: str) -> None:
381 | """Set new target preset mode."""
382 | if preset_mode not in self.preset_modes:
383 | raise ValueError(f"Unsupported preset_mode '{preset_mode}'")
384 |
385 | builder = self.coordinator.get_change_request_builder()
386 | await self._async_exit_summer_house_mode(builder)
387 | builder.set_eco_mode(constants.EcoMode.Auto)
388 | if preset_mode in (PRESET_QUIET, PRESET_ECO):
389 | builder.set_eco_mode(constants.EcoMode.Quiet)
390 | elif preset_mode in (PRESET_POWERFUL, PRESET_BOOST):
391 | builder.set_eco_mode(constants.EcoMode.Powerful)
392 | elif preset_mode == PRESET_8_15:
393 | await self._async_enter_summer_house_mode(builder)
394 | await self.coordinator.async_apply_changes(builder)
395 | self._update_attributes(builder)
396 | await self.coordinator.async_request_refresh()
397 |
398 | async def async_set_fan_mode(self, fan_mode: str) -> None:
399 | """Set new target fan mode."""
400 | if fan_mode not in self.fan_modes:
401 | raise ValueError(f"Unsupported fan_mode '{fan_mode}'")
402 |
403 | builder = self.coordinator.get_change_request_builder()
404 | builder.set_fan_speed(fan_mode)
405 | await self.coordinator.async_apply_changes(builder)
406 | self._update_attributes(builder)
407 |
408 | async def async_set_swing_mode(self, swing_mode: str):
409 | """Set new target swing mode."""
410 | if swing_mode not in self.swing_modes:
411 | raise ValueError(f"Unsupported swing mode '{swing_mode}'")
412 |
413 | builder = self.coordinator.get_change_request_builder()
414 | builder.set_vertical_swing(swing_mode)
415 | await self.coordinator.async_apply_changes(builder)
416 | self._update_attributes(builder)
417 |
418 | async def async_set_swing_horizontal_mode(self, swing_mode: str):
419 | """Set new target swing mode."""
420 | if swing_mode not in self.swing_horizontal_modes:
421 | raise ValueError(f"Unsupported swing mode '{swing_mode}'")
422 |
423 | builder = self.coordinator.get_change_request_builder()
424 | builder.set_horizontal_swing(swing_mode)
425 | await self.coordinator.async_apply_changes(builder)
426 | self._update_attributes(builder)
427 |
428 | class AquareaClimateEntity(AquareaDataEntity, ClimateEntity):
429 | """Representation of a Aquarea Climate Device."""
430 |
431 | _attr_temperature_unit = UnitOfTemperature.CELSIUS
432 | _attr_target_temperature_step = 1
433 | _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
434 | entity_description: AquareaClimateEntityDescription
435 |
436 | def __init__(self, coordinator: AquareaDeviceCoordinator, description: AquareaClimateEntityDescription):
437 | """Initialize the climate entity."""
438 | self.entity_description = description
439 | device = coordinator.device
440 |
441 | self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
442 |
443 |
444 | if device.support_cooling(description.zone_id):
445 | self._attr_hvac_modes.extend([HVACMode.COOL, HVACMode.HEAT_COOL])
446 |
447 | super().__init__(coordinator, description.key)
448 | _LOGGER.info(f"Registing Climate entity: '{self._attr_unique_id}'")
449 |
450 | def _async_update_attrs(self) -> None:
451 | """Update attributes."""
452 | device = self.coordinator.device
453 | zone = device.zones.get(self.entity_description.zone_id)
454 | self._attr_hvac_mode = convert_mode_and_status_to_hvac_mode(device.mode, zone.operation_status)
455 | self._attr_hvac_action = convert_aquarea_action_to_hvac_action(device.current_action)
456 | self._attr_current_temperature = zone.temperature
457 |
458 | self._attr_max_temp = zone.temperature
459 | self._attr_min_temp = zone.temperature
460 |
461 | if zone.supports_set_temperature and device.mode != AquareaExtendedOperationMode.OFF:
462 | self._attr_max_temp = (
463 | zone.cool_max
464 | if device.mode
465 | in (AquareaExtendedOperationMode.COOL, AquareaExtendedOperationMode.AUTO_COOL)
466 | else zone.heat_max
467 | )
468 | self._attr_min_temp = (
469 | zone.cool_min
470 | if device.mode
471 | in (AquareaExtendedOperationMode.COOL, AquareaExtendedOperationMode.AUTO_COOL)
472 | else zone.heat_min
473 | )
474 | self._attr_target_temperature = (
475 | zone.cool_target_temperature
476 | if device.mode
477 | in (
478 | AquareaExtendedOperationMode.COOL,
479 | AquareaExtendedOperationMode.AUTO_COOL,
480 | )
481 | else zone.heat_target_temperature
482 | )
483 |
484 | async def async_turn_on(self) -> None:
485 | """Set the climate state to on."""
486 | await self.coordinator.device.turn_on()
487 | await self.coordinator.async_request_refresh()
488 | self.async_write_ha_state()
489 |
490 | async def async_turn_off(self) -> None:
491 | """Set the climate state to off."""
492 | await self.coordinator.device.turn_off()
493 | self._attr_hvac_mode = HVACMode.OFF
494 | self.async_write_ha_state()
495 |
496 | async def async_set_temperature(self, **kwargs: Any) -> None:
497 | """Set the climate temperature."""
498 | device = self.coordinator.device
499 | zone = device.zones.get(self.entity_description.zone_id)
500 | if mode := kwargs.get(ATTR_HVAC_MODE):
501 | await self.set_hvac_mode(mode)
502 | if temp := kwargs.get(ATTR_TEMPERATURE) and zone.supports_set_temperature:
503 | await self.coordinator.device.set_temperature(int(temp), zone.zone_id)
504 |
505 | async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
506 | """Set new target hvac mode."""
507 | if hvac_mode == HVACMode.OFF:
508 | await self.async_turn_off()
509 | return
510 | if not (op_mode := convert_hvac_mode_to_aquarea_operation_mode(hvac_mode)):
511 | raise ValueError(f"Invalid hvac mode {hvac_mode}")
512 | await self.coordinator.device.set_mode(op_mode, self.entity_description.zone_id)
513 |
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/config_flow.py:
--------------------------------------------------------------------------------
1 | """Config flow for the Panasonic Comfort Cloud platform."""
2 | import asyncio
3 | import logging
4 | from typing import Any, Dict, Optional, Mapping
5 |
6 | import voluptuous as vol
7 | from aiohttp import ClientError
8 | from homeassistant import config_entries
9 | from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
10 | from homeassistant.core import callback
11 | from homeassistant.helpers.aiohttp_client import async_get_clientsession
12 |
13 | from aio_panasonic_comfort_cloud import ApiClient
14 | from . import DOMAIN as PANASONIC_DOMAIN
15 | from .const import (
16 | KEY_DOMAIN,
17 | CONF_FORCE_OUTSIDE_SENSOR,
18 | CONF_ENABLE_DAILY_ENERGY_SENSOR,
19 | DEFAULT_ENABLE_DAILY_ENERGY_SENSOR,
20 | CONF_USE_PANASONIC_PRESET_NAMES,
21 | DEFAULT_USE_PANASONIC_PRESET_NAMES,
22 | CONF_DEVICE_FETCH_INTERVAL,
23 | DEFAULT_DEVICE_FETCH_INTERVAL,
24 | CONF_ENERGY_FETCH_INTERVAL,
25 | DEFAULT_ENERGY_FETCH_INTERVAL,
26 | CONF_FORCE_ENABLE_NANOE,
27 | DEFAULT_FORCE_ENABLE_NANOE)
28 |
29 | _LOGGER = logging.getLogger(__name__)
30 |
31 |
32 | class FlowHandler(config_entries.ConfigFlow, domain=PANASONIC_DOMAIN):
33 | """Handle a config flow."""
34 |
35 | VERSION = 1
36 | CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
37 | _entry: config_entries.ConfigEntry | None = None
38 |
39 | @staticmethod
40 | @callback
41 | def async_get_options_flow(config_entry):
42 | """Get the options flow for this handler."""
43 | return PanasonicOptionsFlowHandler(config_entry)
44 |
45 | async def _create_entry(self, username, password):
46 | """Register new entry."""
47 | # Check if ip already is registered
48 | for entry in self._async_current_entries():
49 | if entry.data[KEY_DOMAIN] == PANASONIC_DOMAIN:
50 | return self.async_abort(reason="already_configured")
51 |
52 | return self.async_create_entry(title="", data={
53 | CONF_USERNAME: username,
54 | CONF_PASSWORD: password,
55 | CONF_FORCE_OUTSIDE_SENSOR: False,
56 | CONF_FORCE_ENABLE_NANOE: DEFAULT_FORCE_ENABLE_NANOE,
57 | CONF_ENABLE_DAILY_ENERGY_SENSOR: DEFAULT_ENABLE_DAILY_ENERGY_SENSOR,
58 | CONF_USE_PANASONIC_PRESET_NAMES: DEFAULT_USE_PANASONIC_PRESET_NAMES,
59 | CONF_DEVICE_FETCH_INTERVAL: DEFAULT_DEVICE_FETCH_INTERVAL,
60 | CONF_ENERGY_FETCH_INTERVAL: DEFAULT_ENERGY_FETCH_INTERVAL,
61 | })
62 |
63 | async def _create_device(self, username, password):
64 | """Create device."""
65 | try:
66 | client = async_get_clientsession(self.hass)
67 | api = ApiClient(username, password, client)
68 | await api.start_session()
69 | devices = api.get_devices()
70 |
71 | if not devices and not api.unknown_devices:
72 | _LOGGER.debug("No devices found")
73 | return self.async_abort(reason="No devices")
74 |
75 | except asyncio.TimeoutError as te:
76 | _LOGGER.exception("TimeoutError", te)
77 | return self.async_abort(reason="device_timeout")
78 | except ClientError as ce:
79 | _LOGGER.exception("ClientError", ce)
80 | return self.async_abort(reason="device_fail")
81 | except Exception as e: # pylint: disable=broad-except
82 | _LOGGER.exception("Unexpected error creating device", e)
83 | return self.async_abort(reason="device_fail")
84 |
85 | return await self._create_entry(username, password)
86 |
87 | async def async_step_user(self, user_input=None):
88 | """User initiated config flow."""
89 |
90 | if user_input is None:
91 | return self.async_show_form(
92 | step_id="user", data_schema=vol.Schema({
93 | vol.Required(CONF_USERNAME): str,
94 | vol.Required(CONF_PASSWORD): str,
95 | vol.Optional(
96 | CONF_ENABLE_DAILY_ENERGY_SENSOR,
97 | default=DEFAULT_ENABLE_DAILY_ENERGY_SENSOR,
98 | ): bool,
99 | vol.Optional(
100 | CONF_FORCE_ENABLE_NANOE,
101 | default=False,
102 | ): bool,
103 | vol.Optional(
104 | CONF_USE_PANASONIC_PRESET_NAMES,
105 | default=DEFAULT_USE_PANASONIC_PRESET_NAMES,
106 | ): bool,
107 | vol.Optional(
108 | CONF_DEVICE_FETCH_INTERVAL,
109 | default=DEFAULT_DEVICE_FETCH_INTERVAL,
110 | ): int,
111 | vol.Optional(
112 | CONF_ENERGY_FETCH_INTERVAL,
113 | default=DEFAULT_ENERGY_FETCH_INTERVAL,
114 | ): int,
115 | })
116 | )
117 | return await self._create_device(user_input[CONF_USERNAME], user_input[CONF_PASSWORD])
118 |
119 | async def async_step_import(self, user_input):
120 | """Import a config entry."""
121 | username = user_input.get(CONF_USERNAME)
122 | if not username:
123 | return await self.async_step_user()
124 | return await self._create_device(username, user_input[CONF_PASSWORD])
125 |
126 | async def async_step_reconfigure(
127 | self, entry_data: Mapping[str, Any]
128 | ) -> config_entries.ConfigFlowResult:
129 | """Handle reauth on failure."""
130 | self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
131 | return await self.async_step_reconfigure_confirm()
132 |
133 | async def async_auth(self, user_input: Mapping[str, Any]) -> dict[str, str]:
134 | """Reusable Auth Helper."""
135 | client = async_get_clientsession(self.hass)
136 | username = user_input[CONF_USERNAME]
137 | password = user_input[CONF_PASSWORD]
138 | api = ApiClient(username, password, client)
139 | try:
140 | await api.reauthenticate()
141 | devices = api.get_devices()
142 |
143 | if not devices and not api.unknown_devices:
144 | return {"base": "no_devices"}
145 | except asyncio.TimeoutError as te:
146 | _LOGGER.exception("TimeoutError", te)
147 | return {"base": "device_timeout"}
148 | except ClientError as ce:
149 | _LOGGER.exception("ClientError", ce)
150 | return {"base": "device_fail"}
151 | except Exception as e: # pylint: disable=broad-except
152 | err_msg = str(e)
153 | if "invalid_user_password" in err_msg:
154 | return {"base": "invalid_user_password"}
155 | _LOGGER.exception("Unexpected error creating device", e)
156 | return {"base": "device_fail"}
157 |
158 |
159 | return {}
160 |
161 | async def async_step_reconfigure_confirm(
162 | self, user_input: Mapping[str, Any] | None = None
163 | ) -> config_entries.ConfigFlowResult:
164 | """Handle users reauth credentials."""
165 |
166 | assert self._entry
167 | errors: dict[str, str] = {}
168 |
169 | if user_input and not (errors := await self.async_auth(user_input)):
170 | return self.async_update_reload_and_abort(
171 | self._entry,
172 | data=user_input,
173 | )
174 |
175 | return self.async_show_form(
176 | step_id="reconfigure_confirm",
177 | data_schema=vol.Schema({
178 | vol.Required(CONF_USERNAME): str,
179 | vol.Required(CONF_PASSWORD): str,
180 | }),
181 | errors=errors,
182 | )
183 |
184 |
185 |
186 | class PanasonicOptionsFlowHandler(config_entries.OptionsFlow):
187 | """Handle Panasonic options."""
188 |
189 | def __init__(self, config_entry):
190 | """Initialize Panasonic options flow."""
191 | self.config_entry = config_entry
192 |
193 | async def async_step_init(
194 | self, user_input: Optional[Dict[str, Any]] = None
195 | ) -> config_entries.ConfigFlowResult:
196 | """Manage Panasonic options."""
197 | if user_input is not None:
198 | return self.async_create_entry(title="", data=user_input)
199 |
200 | return self.async_show_form(
201 | step_id="init",
202 | data_schema=vol.Schema(
203 | {
204 | vol.Optional(
205 | CONF_ENABLE_DAILY_ENERGY_SENSOR,
206 | default=self.config_entry.options.get(
207 | CONF_ENABLE_DAILY_ENERGY_SENSOR, DEFAULT_ENABLE_DAILY_ENERGY_SENSOR
208 | ),
209 | ): bool,
210 | vol.Optional(
211 | CONF_FORCE_ENABLE_NANOE,
212 | default=self.config_entry.options.get(
213 | CONF_FORCE_ENABLE_NANOE, DEFAULT_FORCE_ENABLE_NANOE
214 | ),
215 | ): bool,
216 | vol.Optional(
217 | CONF_USE_PANASONIC_PRESET_NAMES,
218 | default=self.config_entry.options.get(
219 | CONF_USE_PANASONIC_PRESET_NAMES, DEFAULT_USE_PANASONIC_PRESET_NAMES
220 | ),
221 | ): bool,
222 | vol.Optional(
223 | CONF_DEVICE_FETCH_INTERVAL,
224 | default=self.config_entry.options.get(
225 | CONF_DEVICE_FETCH_INTERVAL, DEFAULT_DEVICE_FETCH_INTERVAL
226 | ),
227 | ): int,
228 | vol.Optional(
229 | CONF_ENERGY_FETCH_INTERVAL,
230 | default=self.config_entry.options.get(
231 | CONF_ENERGY_FETCH_INTERVAL, DEFAULT_ENERGY_FETCH_INTERVAL
232 | ),
233 | ): int,
234 | }
235 | ),
236 | )
237 |
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/const.py:
--------------------------------------------------------------------------------
1 | """Constants for Panasonic Cloud."""
2 | from homeassistant.const import CONF_ICON, CONF_NAME, CONF_TYPE, Platform
3 | from homeassistant.components.climate.const import (
4 | HVACMode, ClimateEntityFeature,
5 | PRESET_ECO, PRESET_NONE, PRESET_BOOST)
6 |
7 | ATTR_TARGET_TEMPERATURE = "target_temperature"
8 | ATTR_INSIDE_TEMPERATURE = "inside_temperature"
9 | ATTR_OUTSIDE_TEMPERATURE = "outside_temperature"
10 | ATTR_DAILY_ENERGY = "daily_energy"
11 | ATTR_CURRENT_POWER = "current_power"
12 |
13 | ATTR_SWING_LR_MODE = "horizontal_swing_mode"
14 | ATTR_SWING_LR_MODES = "horizontal_swing_modes"
15 |
16 | ATTR_STATE_ON = "on"
17 | ATTR_STATE_OFF = "off"
18 |
19 | STATE_HEATING = "heating"
20 |
21 | SERVICE_SET_SWING_LR_MODE = "set_horizontal_swing_mode"
22 |
23 | KEY_DOMAIN = "domain"
24 |
25 | TIMEOUT = 60
26 |
27 | CONF_FORCE_OUTSIDE_SENSOR = "force_outside_sensor"
28 | DEFAULT_FORCE_OUTSIDE_SENSOR = False
29 | CONF_ENABLE_DAILY_ENERGY_SENSOR = "enable_daily_energy_sensor"
30 | DEFAULT_ENABLE_DAILY_ENERGY_SENSOR = False
31 | CONF_USE_PANASONIC_PRESET_NAMES = "use_panasonic_preset_names"
32 | DEFAULT_USE_PANASONIC_PRESET_NAMES = True
33 |
34 | SENSOR_TYPE_TEMPERATURE = "temperature"
35 |
36 | PRESET_8_15 = "heat_8_15"
37 | PRESET_QUIET = "quiet"
38 | PRESET_POWERFUL = "powerful"
39 |
40 | SENSOR_TYPES = {
41 | ATTR_INSIDE_TEMPERATURE: {
42 | CONF_NAME: "Inside Temperature",
43 | CONF_ICON: "mdi:thermometer",
44 | CONF_TYPE: SENSOR_TYPE_TEMPERATURE,
45 | },
46 | ATTR_OUTSIDE_TEMPERATURE: {
47 | CONF_NAME: "Outside Temperature",
48 | CONF_ICON: "mdi:thermometer",
49 | CONF_TYPE: SENSOR_TYPE_TEMPERATURE,
50 | },
51 | }
52 |
53 | ENERGY_SENSOR_TYPES = {
54 | ATTR_DAILY_ENERGY: {
55 | CONF_NAME: "Daily Energy",
56 | CONF_ICON: "mdi:flash",
57 | CONF_TYPE: "kWh",
58 | },
59 | ATTR_CURRENT_POWER: {
60 | CONF_NAME: "Current Power",
61 | CONF_ICON: "mdi:flash",
62 | CONF_TYPE: "W",
63 | },
64 | }
65 |
66 | SUPPORT_FLAGS = (
67 | ClimateEntityFeature.TARGET_TEMPERATURE |
68 | ClimateEntityFeature.FAN_MODE |
69 | ClimateEntityFeature.PRESET_MODE |
70 | ClimateEntityFeature.SWING_MODE |
71 | ClimateEntityFeature.TURN_OFF |
72 | ClimateEntityFeature.TURN_ON
73 | )
74 |
75 |
76 |
77 | OPERATION_LIST = {
78 | HVACMode.OFF: 'Off',
79 | HVACMode.HEAT: 'Heat',
80 | HVACMode.COOL: 'Cool',
81 | HVACMode.HEAT_COOL: 'Auto',
82 | HVACMode.DRY: 'Dry',
83 | HVACMode.FAN_ONLY: 'Fan'
84 | }
85 |
86 | DOMAIN = "panasonic_cc"
87 | MANUFACTURER = "Panasonic"
88 | PANASONIC_DEVICES = "panasonic_devices"
89 | DATA_COORDINATORS = "data_coordinators"
90 | ENERGY_COORDINATORS = "energy_coordinators"
91 | AQUAREA_COORDINATORS = "aquarea_coorinators"
92 |
93 | COMPONENT_TYPES = [
94 | Platform.CLIMATE,
95 | Platform.SENSOR,
96 | Platform.SWITCH,
97 | Platform.BUTTON,
98 | Platform.SELECT,
99 | Platform.NUMBER,
100 | Platform.WATER_HEATER
101 | ]
102 |
103 | STARTUP = """
104 | -------------------------------------------------------------------
105 | Panasonic Comfort Cloud
106 |
107 | Version: %s
108 | This is a custom integration
109 | If you have any issues with this you need to open an issue here:
110 | https://github.com/sockless-coding/panasonic_cc/issues
111 | -------------------------------------------------------------------
112 | """
113 |
114 | SELECT_HORIZONTAL_SWING = "horizontal_swing"
115 | SELECT_VERTICAL_SWING = "vertical_swing"
116 |
117 | CONF_DEVICE_FETCH_INTERVAL = "device_fetch_interval"
118 | CONF_ENERGY_FETCH_INTERVAL = "energy_fetch_interval"
119 | CONF_UPDATE_INTERVAL_VERSION = "update_interval_version"
120 | DEFAULT_DEVICE_FETCH_INTERVAL = 120
121 | DEFAULT_ENERGY_FETCH_INTERVAL = 300
122 | CONF_FORCE_ENABLE_NANOE = "force_enable_nanoe"
123 | DEFAULT_FORCE_ENABLE_NANOE = False
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/coordinator.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from datetime import timedelta
4 | from homeassistant.core import HomeAssistant
5 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
6 | from homeassistant.helpers.entity import DeviceInfo
7 | from homeassistant.helpers.storage import Store
8 | from homeassistant.util import dt as dt_util
9 |
10 | from aio_panasonic_comfort_cloud import ApiClient, PanasonicDevice, PanasonicDeviceInfo, PanasonicDeviceEnergy, ChangeRequestBuilder
11 | from aioaquarea import Client as AquareaApiClient, Device as AquareaDevice, AquareaEnvironment
12 | from aioaquarea.data import DeviceInfo as AquareaDeviceInfo
13 |
14 | from .const import DOMAIN,MANUFACTURER, DEFAULT_DEVICE_FETCH_INTERVAL, CONF_DEVICE_FETCH_INTERVAL, CONF_ENERGY_FETCH_INTERVAL, DEFAULT_ENERGY_FETCH_INTERVAL
15 |
16 | _LOGGER = logging.getLogger(__name__)
17 |
18 | class PanasonicDeviceCoordinator(DataUpdateCoordinator[int]):
19 |
20 | def __init__(self, hass: HomeAssistant, config: dict, api_client: ApiClient, device_info: PanasonicDeviceInfo):
21 | super().__init__(
22 | hass,
23 | _LOGGER,
24 | name="Panasonic Device Coordinator",
25 | update_interval=timedelta(seconds=config.get(CONF_DEVICE_FETCH_INTERVAL, DEFAULT_DEVICE_FETCH_INTERVAL)),
26 | update_method=self._fetch_device_data,
27 | )
28 | self._hass = hass
29 | self._config = config
30 | self._api_client = api_client
31 | self._panasonic_device_info = device_info
32 | self._device:PanasonicDevice | None = None
33 | self._store = Store(hass, version=1, key=f"panasonic_cc_{device_info.id}")
34 | self._update_id = 0
35 |
36 |
37 | @property
38 | def device(self) -> PanasonicDevice:
39 | if self._device is None:
40 | raise ValueError("device has not been initialized")
41 | return self._device
42 |
43 | @property
44 | def api_client(self) -> ApiClient:
45 | return self._api_client
46 |
47 | @property
48 | def device_id(self) -> str:
49 | return self._panasonic_device_info.id
50 |
51 |
52 | @property
53 | def device_info(self)->DeviceInfo:
54 | return DeviceInfo(
55 | identifiers={(DOMAIN, self._panasonic_device_info.id )},
56 | manufacturer=MANUFACTURER,
57 | model=self._panasonic_device_info.model,
58 | name=self._panasonic_device_info.name,
59 | sw_version=self._api_client.app_version
60 | )
61 |
62 | def get_change_request_builder(self):
63 | return ChangeRequestBuilder(self.device)
64 |
65 | async def async_apply_changes(self, request_builder: ChangeRequestBuilder):
66 | await self._api_client.set_device_raw(self.device, request_builder.build())
67 |
68 | async def async_get_stored_data(self):
69 | data = await self._store.async_load()
70 | if data is None:
71 | data = {}
72 | return data
73 |
74 | async def async_store_data(self, data):
75 | await self._store.async_save(data)
76 |
77 |
78 | async def _fetch_device_data(self)->int:
79 | try:
80 | if self._device is None:
81 | self._device = await self._api_client.get_device(self._panasonic_device_info)
82 | _LOGGER.debug(
83 | "%s Device features\nNanoe: %s\nEco Navi: %s\nAI Eco: %s",
84 | self._panasonic_device_info.name,
85 | self._device.has_nanoe,
86 | self._device.has_eco_navi,
87 | self._device.has_eco_function)
88 | self._update_id = 1
89 | return self._update_id
90 | if await self._api_client.try_update_device(self._device):
91 | self._update_id = self._update_id + 1
92 | return self._update_id
93 | except BaseException as e:
94 | _LOGGER.error("Error fetching device data from API: %s", e, exc_info=e)
95 | raise UpdateFailed(f"Invalid response from API: {e}") from e
96 | return self._update_id
97 |
98 | class PanasonicDeviceEnergyCoordinator(DataUpdateCoordinator[int]):
99 |
100 | def __init__(self, hass: HomeAssistant, config: dict, api_client: ApiClient, device_info: PanasonicDeviceInfo):
101 | super().__init__(
102 | hass,
103 | _LOGGER,
104 | name="Panasonic Device Energy Coordinator",
105 | update_interval=timedelta(seconds=config.get(CONF_ENERGY_FETCH_INTERVAL, DEFAULT_ENERGY_FETCH_INTERVAL)),
106 | update_method=self._fetch_device_data,
107 | )
108 | self._hass = hass
109 | self._config = config
110 | self._api_client = api_client
111 | self._panasonic_device_info = device_info
112 | self._energy: PanasonicDeviceEnergy | None = None
113 | self._update_id = 0
114 |
115 | @property
116 | def api_client(self) -> ApiClient:
117 | return self._api_client
118 |
119 | @property
120 | def device_id(self) -> str:
121 | return self._panasonic_device_info.id
122 |
123 | @property
124 | def energy(self) -> PanasonicDeviceEnergy | None:
125 | return self._energy
126 |
127 | @property
128 | def device_info(self)->DeviceInfo:
129 | return DeviceInfo(
130 | identifiers={(DOMAIN, self._panasonic_device_info.id )},
131 | manufacturer=MANUFACTURER,
132 | model=self._panasonic_device_info.model,
133 | name=self._panasonic_device_info.name,
134 | sw_version=self._api_client.app_version
135 | )
136 |
137 | async def _fetch_device_data(self)->int:
138 | try:
139 | if self._energy is None:
140 | self._energy = await self._api_client.async_get_energy(self._panasonic_device_info)
141 | self._update_id = 1
142 | return self._update_id
143 | if await self._api_client.async_try_update_energy(self._energy):
144 | self._update_id = self._update_id + 1
145 | return self._update_id
146 | except BaseException as e:
147 | _LOGGER.error("Error fetching energy data from API: %s", e, exc_info=e)
148 | raise UpdateFailed(f"Invalid response from API: {e}") from e
149 | return self._update_id
150 |
151 |
152 | class AquareaDeviceCoordinator(DataUpdateCoordinator):
153 |
154 | def __init__(self, hass: HomeAssistant, config: dict, api_client: AquareaApiClient, device_info: AquareaDeviceInfo):
155 | super().__init__(
156 | hass,
157 | _LOGGER,
158 | name="Aquarea Device Coordinator",
159 | update_interval=timedelta(seconds=config.get(CONF_DEVICE_FETCH_INTERVAL, DEFAULT_DEVICE_FETCH_INTERVAL)),
160 | update_method=self._fetch_device_data,
161 | )
162 | self._hass = hass
163 | self._config = config
164 | self._api_client = api_client
165 | self._aquarea_device_info = device_info
166 | self._device:AquareaDevice | None = None
167 | self._update_id = 0
168 | self._is_demo = api_client._environment == AquareaEnvironment.DEMO
169 |
170 | @property
171 | def device(self) -> AquareaDevice:
172 | if self._device is None:
173 | raise ValueError("device has not been initialized")
174 | return self._device
175 |
176 | @property
177 | def api_client(self) -> AquareaApiClient:
178 | return self._api_client
179 |
180 | @property
181 | def device_id(self) -> str:
182 | return self.device.device_id if not self._is_demo else "demo-house"
183 |
184 |
185 | @property
186 | def device_info(self)->DeviceInfo:
187 | return DeviceInfo(
188 | identifiers={(DOMAIN, self.device_id)},
189 | manufacturer=self.device.manufacturer,
190 | model="",
191 | name=self.device.name,
192 | sw_version=self.device.version,
193 | )
194 |
195 | async def _fetch_device_data(self)->int:
196 | try:
197 | if self._device is None:
198 | self._device = await self._api_client.get_device(
199 | device_info=self._aquarea_device_info,
200 | consumption_refresh_interval=timedelta(seconds=self._config.get(CONF_ENERGY_FETCH_INTERVAL, DEFAULT_ENERGY_FETCH_INTERVAL)),
201 | timezone=dt_util.DEFAULT_TIME_ZONE)
202 |
203 | self._update_id = 1
204 | return self._update_id
205 | await self._device.refresh_data()
206 | self._update_id = self._update_id + 1
207 | return self._update_id
208 | except BaseException as e:
209 | _LOGGER.error("Error fetching device data from API: %s", e, exc_info=e)
210 | raise UpdateFailed(f"Invalid response from API: {e}") from e
211 | return self._update_id
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/icons.json:
--------------------------------------------------------------------------------
1 | {
2 | "entity": {
3 | "climate": {
4 | "climate":{
5 | "state_attributes": {
6 | "preset_mode": {
7 | "state": {
8 | "none": "mdi:air-filter",
9 | "heat_8_15": "mdi:weather-sunny",
10 | "quiet": "mdi:leaf",
11 | "powerful": "mdi:arm-flex"
12 | }
13 | },
14 | "swing_mode":{
15 | "state": {
16 | "Auto": "mdi:shuffle",
17 | "Up": "mdi:arrow-up-thin",
18 | "UpMid": "mdi:arrow-top-right-thin",
19 | "Mid": "mdi:arrow-right-thin",
20 | "DownMid": "mdi:arrow-bottom-right-thin",
21 | "Down": "mdi:arrow-down-thin",
22 | "Swing": "mdi:shuffle"
23 | }
24 | },
25 | "swing_horizontal_mode":{
26 | "state": {
27 | "Auto": "mdi:compare-horizontal",
28 | "Left": "mdi:arrow-left-thin",
29 | "LeftMid": "mdi:arrow-bottom-left-thin",
30 | "Mid": "mdi:arrow-down-thin",
31 | "RightMid": "mdi:arrow-bottom-right-thin",
32 | "Right": "mdi:arrow-right-thin"
33 | }
34 | },
35 | "fan_mode":{
36 | "state": {
37 | "Auto": "mdi:alpha-a-circle-outline",
38 | "Low": "mdi:numeric-1-circle-outline",
39 | "LowMid": "mdi:numeric-2-circle-outline",
40 | "Mid": "mdi:numeric-3-circle-outline",
41 | "HighMid": "mdi:numeric-4-circle-outline",
42 | "High": "mdi:numeric-5-circle-outline"
43 | }
44 | }
45 | }
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "domain": "panasonic_cc",
3 | "name": "Panasonic Comfort Cloud",
4 | "after_dependencies": ["http"],
5 | "version": "2025.5.0",
6 | "config_flow": true,
7 | "documentation": "https://github.com/sockless-coding/panasonic_cc/",
8 | "dependencies": [],
9 | "codeowners": ["@sockless-coding","@craibo","@cjaliaga"],
10 | "integration_type": "hub",
11 | "iot_class": "cloud_polling",
12 | "issue_tracker": "https://github.com/sockless-coding/panasonic_cc/issues",
13 | "requirements": ["aiohttp","aio-panasonic-comfort-cloud==2025.5.1","aioaquarea==0.7.2"],
14 | "quality_scale": "silver"
15 | }
16 |
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/number.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import Callable
3 | from dataclasses import dataclass
4 |
5 | from homeassistant.const import PERCENTAGE
6 | from homeassistant.core import HomeAssistant
7 | from homeassistant.components.number import (
8 | NumberDeviceClass,
9 | NumberEntity,
10 | NumberEntityDescription,
11 | NumberMode,
12 | )
13 |
14 | from aio_panasonic_comfort_cloud import PanasonicDevice, PanasonicDeviceZone, ChangeRequestBuilder
15 |
16 | from . import DOMAIN
17 | from .const import DATA_COORDINATORS
18 | from .coordinator import PanasonicDeviceCoordinator
19 | from .base import PanasonicDataEntity
20 |
21 | @dataclass(frozen=True, kw_only=True)
22 | class PanasonicNumberEntityDescription(NumberEntityDescription):
23 | """Describes Panasonic Number entity."""
24 | get_value: Callable[[PanasonicDevice], int]
25 | set_value: Callable[[ChangeRequestBuilder, int], ChangeRequestBuilder]
26 |
27 | def create_zone_damper_description(zone: PanasonicDeviceZone):
28 | return PanasonicNumberEntityDescription(
29 | key = f"zone-{zone.id}-damper",
30 | translation_key=f"zone-{zone.id}-damper",
31 | name = f"{zone.name} Damper Position",
32 | icon="mdi:valve",
33 | native_unit_of_measurement=PERCENTAGE,
34 | native_max_value=100,
35 | native_min_value=0,
36 | native_step=10,
37 | mode=NumberMode.SLIDER,
38 | get_value=lambda device: zone.level,
39 | set_value=lambda builder, value: builder.set_zone_damper(zone.id, value),
40 | )
41 |
42 | async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities):
43 | devices = []
44 | data_coordinators: list[PanasonicDeviceCoordinator] = hass.data[DOMAIN][DATA_COORDINATORS]
45 | for data_coordinator in data_coordinators:
46 | if data_coordinator.device.has_zones:
47 | for zone in data_coordinator.device.parameters.zones:
48 | devices.append(PanasonicNumberEntity(
49 | data_coordinator,
50 | create_zone_damper_description(zone)))
51 |
52 | async_add_entities(devices)
53 |
54 | class PanasonicNumberEntity(PanasonicDataEntity, NumberEntity):
55 |
56 | entity_description: PanasonicNumberEntityDescription
57 |
58 | def __init__(self, coordinator: PanasonicDeviceCoordinator, description: PanasonicNumberEntityDescription):
59 | self.entity_description = description
60 | super().__init__(coordinator, description.key)
61 |
62 |
63 | async def async_set_native_value(self, value: float) -> None:
64 | """Set new value."""
65 | value = int(value)
66 | builder = self.coordinator.get_change_request_builder()
67 | self.entity_description.set_value(builder, value)
68 | await self.coordinator.async_apply_changes(builder)
69 | self._attr_native_value = value
70 | self.async_write_ha_state()
71 |
72 | def _async_update_attrs(self) -> None:
73 | self._attr_native_value = self.entity_description.get_value(self.coordinator.device)
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/select.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 | from dataclasses import dataclass
3 |
4 | from homeassistant.core import HomeAssistant
5 | from homeassistant.components.select import SelectEntity, SelectEntityDescription
6 |
7 | from .const import DOMAIN, DATA_COORDINATORS, SELECT_HORIZONTAL_SWING, SELECT_VERTICAL_SWING
8 | from aio_panasonic_comfort_cloud import PanasonicDevice, ChangeRequestBuilder, constants
9 |
10 | from .coordinator import PanasonicDeviceCoordinator
11 | from .base import PanasonicDataEntity
12 |
13 | @dataclass(frozen=True, kw_only=True)
14 | class PanasonicSelectEntityDescription(SelectEntityDescription):
15 | """Description of a select entity."""
16 | set_option: Callable[[ChangeRequestBuilder, str], ChangeRequestBuilder]
17 | get_current_option: Callable[[PanasonicDevice], str]
18 | is_available: Callable[[PanasonicDevice], bool]
19 | get_options: Callable[[PanasonicDevice], list[str]] = None
20 |
21 |
22 | HORIZONTAL_SWING_DESCRIPTION = PanasonicSelectEntityDescription(
23 | key=SELECT_HORIZONTAL_SWING,
24 | translation_key=SELECT_HORIZONTAL_SWING,
25 | icon="mdi:swap-horizontal",
26 | name="Horizontal Swing Mode",
27 | options= [opt.name for opt in constants.AirSwingLR if opt != constants.AirSwingLR.Unavailable],
28 | set_option = lambda builder, new_value : builder.set_horizontal_swing(new_value),
29 | get_current_option = lambda device : device.parameters.horizontal_swing_mode.name,
30 | is_available = lambda device : device.has_horizontal_swing
31 | )
32 | VERTICAL_SWING_DESCRIPTION = PanasonicSelectEntityDescription(
33 | key=SELECT_VERTICAL_SWING,
34 | translation_key=SELECT_VERTICAL_SWING,
35 | icon="mdi:swap-vertical",
36 | name="Vertical Swing Mode",
37 | get_options= lambda device: [opt.name for opt in constants.AirSwingUD if opt != constants.AirSwingUD.Swing or device.features.auto_swing_ud],
38 | set_option = lambda builder, new_value : builder.set_vertical_swing(new_value),
39 | get_current_option = lambda device : device.parameters.vertical_swing_mode.name,
40 | is_available = lambda device : True
41 | )
42 |
43 |
44 | async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities):
45 | entities = []
46 | data_coordinators: list[PanasonicDeviceCoordinator] = hass.data[DOMAIN][DATA_COORDINATORS]
47 | for coordinator in data_coordinators:
48 | entities.append(PanasonicSelectEntity(coordinator, HORIZONTAL_SWING_DESCRIPTION))
49 | entities.append(PanasonicSelectEntity(coordinator, VERTICAL_SWING_DESCRIPTION))
50 |
51 | async_add_entities(entities)
52 |
53 | class PanasonicSelectEntityBase(SelectEntity):
54 | """Base class for all select entities."""
55 | entity_description: PanasonicSelectEntityDescription
56 |
57 | class PanasonicSelectEntity(PanasonicDataEntity, PanasonicSelectEntityBase):
58 |
59 | def __init__(self, coordinator: PanasonicDeviceCoordinator, description: PanasonicSelectEntityDescription):
60 | self.entity_description = description
61 | if description.get_options is not None:
62 | self._attr_options = description.get_options(coordinator.device)
63 | super().__init__(coordinator, description.key)
64 |
65 | @property
66 | def available(self) -> bool:
67 | """Return if entity is available."""
68 | return self.entity_description.is_available(self.coordinator.device)
69 |
70 | async def async_select_option(self, option: str) -> None:
71 | builder = self.coordinator.get_change_request_builder()
72 | self.entity_description.set_option(builder, option)
73 | await self.coordinator.async_apply_changes(builder)
74 | self._attr_current_option = option
75 | self.async_write_ha_state()
76 |
77 | def _async_update_attrs(self) -> None:
78 | self.current_option = self.entity_description.get_current_option(self.coordinator.device)
79 |
80 |
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/sensor.py:
--------------------------------------------------------------------------------
1 | from typing import Callable, Any
2 | from dataclasses import dataclass
3 | import logging
4 |
5 | from homeassistant.const import UnitOfTemperature, EntityCategory
6 | from homeassistant.components.sensor import (
7 | SensorEntity,
8 | SensorStateClass,
9 | SensorDeviceClass,
10 | SensorEntityDescription
11 | )
12 |
13 | from aio_panasonic_comfort_cloud import PanasonicDevice, PanasonicDeviceEnergy, PanasonicDeviceZone, constants
14 | from aioaquarea import Device as AquareaDevice
15 |
16 | from .const import (
17 | DOMAIN,
18 | DATA_COORDINATORS,
19 | ENERGY_COORDINATORS,
20 | AQUAREA_COORDINATORS
21 | )
22 | from .base import PanasonicDataEntity, PanasonicEnergyEntity, AquareaDataEntity
23 | from .coordinator import PanasonicDeviceCoordinator, PanasonicDeviceEnergyCoordinator, AquareaDeviceCoordinator
24 |
25 | _LOGGER = logging.getLogger(__name__)
26 |
27 | @dataclass(frozen=True, kw_only=True)
28 | class PanasonicSensorEntityDescription(SensorEntityDescription):
29 | """Describes Panasonic sensor entity."""
30 | get_state: Callable[[PanasonicDevice], Any] | None = None
31 | is_available: Callable[[PanasonicDevice], bool] | None = None
32 |
33 | @dataclass(frozen=True, kw_only=True)
34 | class PanasonicEnergySensorEntityDescription(SensorEntityDescription):
35 | """Describes Panasonic sensor entity."""
36 | get_state: Callable[[PanasonicDeviceEnergy], Any]| None = None
37 |
38 | @dataclass(frozen=True, kw_only=True)
39 | class AquareaSensorEntityDescription(SensorEntityDescription):
40 | """Describes Aquarea sensor entity."""
41 | get_state: Callable[[AquareaDevice], Any] | None = None
42 | is_available: Callable[[AquareaDevice], bool]| None = None
43 |
44 | INSIDE_TEMPERATURE_DESCRIPTION = PanasonicSensorEntityDescription(
45 | key="inside_temperature",
46 | translation_key="inside_temperature",
47 | name="Inside Temperature",
48 | icon="mdi:thermometer",
49 | device_class=SensorDeviceClass.TEMPERATURE,
50 | state_class=SensorStateClass.MEASUREMENT,
51 | native_unit_of_measurement=UnitOfTemperature.CELSIUS,
52 | get_state=lambda device: device.parameters.inside_temperature,
53 | is_available=lambda device: device.parameters.inside_temperature is not None,
54 | )
55 | OUTSIDE_TEMPERATURE_DESCRIPTION = PanasonicSensorEntityDescription(
56 | key="outside_temperature",
57 | translation_key="outside_temperature",
58 | name="Outside Temperature",
59 | icon="mdi:thermometer",
60 | device_class=SensorDeviceClass.TEMPERATURE,
61 | state_class=SensorStateClass.MEASUREMENT,
62 | native_unit_of_measurement=UnitOfTemperature.CELSIUS,
63 | get_state=lambda device: device.parameters.outside_temperature,
64 | is_available=lambda device: device.parameters.outside_temperature is not None,
65 | )
66 | LAST_UPDATE_TIME_DESCRIPTION = PanasonicSensorEntityDescription(
67 | key="last_update",
68 | translation_key="last_update",
69 | name="Last Updated",
70 | icon="mdi:clock-outline",
71 | device_class=SensorDeviceClass.TIMESTAMP,
72 | entity_category=EntityCategory.DIAGNOSTIC,
73 | state_class=None,
74 | native_unit_of_measurement=None,
75 | get_state=lambda device: device.last_update,
76 | is_available=lambda device: True,
77 | entity_registry_enabled_default=False,
78 | )
79 | DATA_AGE_DESCRIPTION = PanasonicSensorEntityDescription(
80 | key="data_age",
81 | translation_key="data_age",
82 | name="Cached Data Age",
83 | icon="mdi:clock-outline",
84 | device_class=SensorDeviceClass.TIMESTAMP,
85 | entity_category=EntityCategory.DIAGNOSTIC,
86 | state_class=None,
87 | native_unit_of_measurement=None,
88 | get_state=lambda device: device.timestamp,
89 | is_available=lambda device: device.info.status_data_mode == constants.StatusDataMode.CACHED,
90 | entity_registry_enabled_default=False,
91 | )
92 | DATA_MODE_DESCRIPTION = PanasonicSensorEntityDescription(
93 | key="status_data_mode",
94 | translation_key="status_data_mode",
95 | name="Data Mode",
96 | options=[opt.name for opt in constants.StatusDataMode],
97 | device_class=SensorDeviceClass.ENUM,
98 | entity_category=EntityCategory.DIAGNOSTIC,
99 | state_class=None,
100 | native_unit_of_measurement=None,
101 | get_state=lambda device: device.info.status_data_mode.name,
102 | is_available=lambda device: True,
103 | entity_registry_enabled_default=True,
104 | )
105 | DAILY_ENERGY_DESCRIPTION = PanasonicEnergySensorEntityDescription(
106 | key="daily_energy_sensor",
107 | translation_key="daily_energy_sensor",
108 | name="Daily Energy",
109 | icon="mdi:flash",
110 | device_class=SensorDeviceClass.ENERGY,
111 | state_class=SensorStateClass.TOTAL_INCREASING,
112 | native_unit_of_measurement="kWh",
113 | get_state=lambda energy: energy.consumption
114 | )
115 | DAILY_HEATING_ENERGY_DESCRIPTION = PanasonicEnergySensorEntityDescription(
116 | key="daily_heating_energy",
117 | translation_key="daily_heating_energy",
118 | name="Daily Heating Energy",
119 | icon="mdi:flash",
120 | device_class=SensorDeviceClass.ENERGY,
121 | state_class=SensorStateClass.TOTAL_INCREASING,
122 | native_unit_of_measurement="kWh",
123 | get_state=lambda energy: energy.heating_consumption
124 | )
125 | DAILY_COOLING_ENERGY_DESCRIPTION = PanasonicEnergySensorEntityDescription(
126 | key="daily_cooling_energy",
127 | translation_key="daily_cooling_energy",
128 | name="Daily Cooling Energy",
129 | icon="mdi:flash",
130 | device_class=SensorDeviceClass.ENERGY,
131 | state_class=SensorStateClass.TOTAL_INCREASING,
132 | native_unit_of_measurement="kWh",
133 | get_state=lambda energy: energy.cooling_consumption
134 | )
135 | POWER_DESCRIPTION = PanasonicEnergySensorEntityDescription(
136 | key="current_power",
137 | translation_key="current_power",
138 | name="Current Extrapolated Power",
139 | icon="mdi:flash",
140 | device_class=SensorDeviceClass.POWER,
141 | state_class=SensorStateClass.MEASUREMENT,
142 | native_unit_of_measurement="W",
143 | get_state=lambda energy: energy.current_power
144 | )
145 | COOLING_POWER_DESCRIPTION = PanasonicEnergySensorEntityDescription(
146 | key="cooling_power",
147 | translation_key="cooling_power",
148 | name="Cooling Extrapolated Power",
149 | icon="mdi:flash",
150 | device_class=SensorDeviceClass.POWER,
151 | state_class=SensorStateClass.MEASUREMENT,
152 | native_unit_of_measurement="W",
153 | get_state=lambda energy: energy.cooling_power
154 | )
155 | HEATING_POWER_DESCRIPTION = PanasonicEnergySensorEntityDescription(
156 | key="heating_power",
157 | translation_key="heating_power",
158 | name="Heating Extrapolated Power",
159 | icon="mdi:flash",
160 | device_class=SensorDeviceClass.POWER,
161 | state_class=SensorStateClass.MEASUREMENT,
162 | native_unit_of_measurement="W",
163 | get_state=lambda energy: energy.heating_power
164 | )
165 |
166 | AQUAREA_OUTSIDE_TEMPERATURE_DESCRIPTION = AquareaSensorEntityDescription(
167 | key="outside_temperature",
168 | translation_key="outside_temperature",
169 | name="Outside Temperature",
170 | icon="mdi:thermometer",
171 | device_class=SensorDeviceClass.TEMPERATURE,
172 | state_class=SensorStateClass.MEASUREMENT,
173 | native_unit_of_measurement=UnitOfTemperature.CELSIUS,
174 | get_state=lambda device: device.temperature_outdoor,
175 | is_available=lambda device: device.temperature_outdoor is not None,
176 | )
177 |
178 | def create_zone_temperature_description(zone: PanasonicDeviceZone):
179 | return PanasonicSensorEntityDescription(
180 | key = f"zone-{zone.id}-temperature",
181 | translation_key=f"zone-{zone.id}-temperature",
182 | name = f"{zone.name} Temperature",
183 | icon="mdi:thermometer",
184 | device_class=SensorDeviceClass.TEMPERATURE,
185 | state_class=SensorStateClass.MEASUREMENT,
186 | native_unit_of_measurement=UnitOfTemperature.CELSIUS,
187 | get_state=lambda device: zone.temperature,
188 | is_available=lambda device: zone.has_temperature
189 | )
190 |
191 |
192 | async def async_setup_entry(hass, entry, async_add_entities):
193 | entities = []
194 | data_coordinators: list[PanasonicDeviceCoordinator] = hass.data[DOMAIN][DATA_COORDINATORS]
195 | energy_coordinators: list[PanasonicDeviceEnergyCoordinator] = hass.data[DOMAIN][ENERGY_COORDINATORS]
196 | aquarea_coordinators: list[AquareaDeviceCoordinator] = hass.data[DOMAIN][AQUAREA_COORDINATORS]
197 |
198 | for coordinator in data_coordinators:
199 | entities.append(PanasonicSensorEntity(coordinator, INSIDE_TEMPERATURE_DESCRIPTION))
200 | entities.append(PanasonicSensorEntity(coordinator, OUTSIDE_TEMPERATURE_DESCRIPTION))
201 | entities.append(PanasonicSensorEntity(coordinator, LAST_UPDATE_TIME_DESCRIPTION))
202 | entities.append(PanasonicSensorEntity(coordinator, DATA_AGE_DESCRIPTION))
203 | entities.append(PanasonicSensorEntity(coordinator, DATA_MODE_DESCRIPTION))
204 | if coordinator.device.has_zones:
205 | for zone in coordinator.device.parameters.zones:
206 | entities.append(PanasonicSensorEntity(
207 | coordinator,
208 | create_zone_temperature_description(zone)))
209 |
210 | for coordinator in energy_coordinators:
211 | entities.append(PanasonicEnergySensorEntity(coordinator, DAILY_ENERGY_DESCRIPTION))
212 | entities.append(PanasonicEnergySensorEntity(coordinator, DAILY_COOLING_ENERGY_DESCRIPTION))
213 | entities.append(PanasonicEnergySensorEntity(coordinator, DAILY_HEATING_ENERGY_DESCRIPTION))
214 | entities.append(PanasonicEnergySensorEntity(coordinator, POWER_DESCRIPTION))
215 | entities.append(PanasonicEnergySensorEntity(coordinator, COOLING_POWER_DESCRIPTION))
216 | entities.append(PanasonicEnergySensorEntity(coordinator, HEATING_POWER_DESCRIPTION))
217 |
218 | for coordinator in aquarea_coordinators:
219 | entities.append(AquareaSensorEntity(coordinator, AQUAREA_OUTSIDE_TEMPERATURE_DESCRIPTION))
220 |
221 | async_add_entities(entities)
222 |
223 |
224 | class PanasonicSensorEntityBase(SensorEntity):
225 | """Base class for all sensor entities."""
226 | entity_description: PanasonicSensorEntityDescription # type: ignore[override]
227 |
228 | class PanasonicSensorEntity(PanasonicDataEntity, PanasonicSensorEntityBase):
229 |
230 | def __init__(self, coordinator: PanasonicDeviceCoordinator, description: PanasonicSensorEntityDescription):
231 | self.entity_description = description
232 | super().__init__(coordinator, description.key)
233 |
234 | @property
235 | def available(self) -> bool:
236 | """Return if entity is available."""
237 | if self.entity_description.is_available is None:
238 | return False
239 | return self.entity_description.is_available(self.coordinator.device)
240 |
241 | def _async_update_attrs(self) -> None:
242 | """Update the attributes of the sensor."""
243 | if self.entity_description.is_available:
244 | self._attr_available = self.entity_description.is_available(self.coordinator.device)
245 | if self.entity_description.get_state:
246 | self._attr_native_value = self.entity_description.get_state(self.coordinator.device)
247 |
248 | class PanasonicEnergySensorEntity(PanasonicEnergyEntity, SensorEntity):
249 |
250 | entity_description: PanasonicEnergySensorEntityDescription # type: ignore[override]
251 |
252 | def __init__(self, coordinator: PanasonicDeviceEnergyCoordinator, description: PanasonicEnergySensorEntityDescription):
253 | self.entity_description = description
254 | super().__init__(coordinator, description.key)
255 |
256 | @property
257 | def available(self) -> bool:
258 | """Return if entity is available."""
259 | return self._attr_available
260 |
261 | def _async_update_attrs(self) -> None:
262 | """Update the attributes of the sensor."""
263 | value = self.entity_description.get_state(self.coordinator.energy)
264 | self._attr_available = value is not None
265 | self._attr_native_value = value
266 |
267 | class AquareaSensorEntity(AquareaDataEntity, SensorEntity):
268 |
269 | entity_description: AquareaSensorEntityDescription
270 |
271 | def __init__(self, coordinator: AquareaDeviceCoordinator, description: AquareaSensorEntityDescription):
272 | self.entity_description = description
273 | super().__init__(coordinator, description.key)
274 |
275 | @property
276 | def available(self) -> bool:
277 | """Return if entity is available."""
278 | value = self.entity_description.is_available(self.coordinator.device) if self.entity_description.is_available else None
279 | return value if value is not None else False
280 |
281 | def _async_update_attrs(self) -> None:
282 | """Update the attributes of the sensor."""
283 | if self.entity_description.is_available:
284 | self._attr_available = self.entity_description.is_available(self.coordinator.device)
285 | if self.entity_description.get_state:
286 | self._attr_native_value = self.entity_description.get_state(self.coordinator.device)
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/services.yaml:
--------------------------------------------------------------------------------
1 | set_horizontal_swing_mode:
2 | description: Set the horizontal swing operation for climate device.
3 | fields:
4 | entity_id:
5 | description: Name(s) of entities to change.
6 | example: "climate.nest"
7 | swing_mode:
8 | description: New value of swing mode.
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/strings.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "step": {
4 | "user": {
5 | "title": "Panasonic Comfort Cloud",
6 | "description": "Enter your Panasonic ID and password",
7 | "data": {
8 | "username": "Panasonic ID",
9 | "password": "Password",
10 | "enable_daily_energy_sensor": "Enable daily energy sensors",
11 | "force_enable_nanoe": "Enable Nanoe switch for all devices",
12 | "use_panasonic_preset_names": "Use 'Quiet' and 'Powerful' instead of 'Eco' and 'Boost' Presets",
13 | "device_fetch_interval": "Device fetch interval (seconds)",
14 | "energy_fetch_interval": "Energy fetch interval (seconds)"
15 | }
16 | },
17 | "reconfigure_confirm": {
18 | "title": "Reconfigure Panasonic Comfort Cloud",
19 | "description": "Enter your Panasonic ID and password to re-authenticate",
20 | "data": {
21 | "username": "Panasonic ID",
22 | "password": "Password"
23 | }
24 | }
25 | },
26 | "error":{
27 | "no_devices": "No devices found. Please check your the account and CFC app and try again.",
28 | "device_timeout": "Timeout connecting to the API.",
29 | "device_fail": "Unexpected error connecting to the API.",
30 | "invalid_user_password": "Invalid Panasonic ID or password."
31 | },
32 | "abort": {
33 | "device_timeout": "Timeout connecting to the device.",
34 | "device_fail": "Unexpected error creating device.",
35 | "already_configured": "Device is already configured",
36 | "reauth_successful": "Re-authentication successful."
37 | }
38 | },
39 | "options": {
40 | "step": {
41 | "init": {
42 | "data": {
43 | "force_outside_sensor": "Force outside sensor",
44 | "enable_daily_energy_sensor": "Enable daily energy sensors (requires restart)",
45 | "force_enable_nanoe": "Enable Nanoe switch for all devices (requires restart)",
46 | "use_panasonic_preset_names": "Use 'Quiet' and 'Powerful' instead of 'Eco' and 'Boost' Presets (requires restart)",
47 | "device_fetch_interval": "Device fetch interval (seconds)",
48 | "energy_fetch_interval": "Energy fetch interval (seconds)"
49 | }
50 | }
51 | }
52 | },
53 | "entity": {
54 | "climate": {
55 | "climate": {
56 | "state_attributes": {
57 | "preset_mode": {
58 | "state": {
59 | "heat_8_15": "+8/15°C",
60 | "none": "None",
61 | "quiet": "Quiet",
62 | "powerful": "Powerful"
63 | }
64 | },
65 | "swing_mode":{
66 | "state": {
67 | "Auto": "Auto",
68 | "Up": "Top",
69 | "UpMid": "Mid-top",
70 | "Mid": "Middle",
71 | "DownMid": "Mid-bottom",
72 | "Down": "Bottom",
73 | "Swing": "Swing"
74 | }
75 | },
76 | "swing_horizontal_mode":{
77 | "state": {
78 | "Auto": "Auto",
79 | "Left": "Left",
80 | "LeftMid": "Center left",
81 | "Mid": "Center",
82 | "RightMid": "Center right",
83 | "Right": "Right"
84 | }
85 | },
86 | "fan_mode":{
87 | "state": {
88 | "Auto": "Auto",
89 | "Low": "Low",
90 | "LowMid": "Medium-Low",
91 | "Mid": "Medium",
92 | "HighMid": "Medium-High",
93 | "High": "High"
94 | }
95 | }
96 | }
97 | }
98 | },
99 | "select": {
100 | "horizontal_swing":{
101 | "state": {
102 | "Auto": "Auto",
103 | "Left": "Left",
104 | "LeftMid": "Center left",
105 | "Mid": "Center",
106 | "RightMid": "Center right",
107 | "Right": "Right"
108 | }
109 | },
110 | "vertical_swing":{
111 | "state": {
112 | "Auto": "Auto",
113 | "Up": "Top",
114 | "UpMid": "Mid-top",
115 | "Mid": "Middle",
116 | "DownMid": "Mid-bottom",
117 | "Down": "Bottom",
118 | "Swing": "Swing"
119 | }
120 | }
121 | }
122 | },
123 | "services": {
124 | "set_horizontal_swing_mode": {
125 | "name": "Set the horizontal swing mode",
126 | "description": "Set the horizontal swing mode for climate device.",
127 | "fields": {
128 | "entity_id": {
129 | "name": "Entity id",
130 | "description": "Name(s) of entities to change."
131 | },
132 | "swing_mode": {
133 | "name": "Swing mode",
134 | "description": "New horizontal swing mode."
135 | }
136 | }
137 | }
138 | }
139 | }
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/switch.py:
--------------------------------------------------------------------------------
1 | """Support for Panasonic Nanoe."""
2 | import logging
3 | from typing import Callable
4 | from dataclasses import dataclass
5 |
6 | from homeassistant.core import HomeAssistant
7 | from homeassistant.config_entries import ConfigEntry
8 | from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity, SwitchEntityDescription
9 | from aio_panasonic_comfort_cloud import constants, PanasonicDevice, PanasonicDeviceZone, ChangeRequestBuilder
10 |
11 |
12 | from . import DOMAIN
13 | from .const import DATA_COORDINATORS, CONF_FORCE_ENABLE_NANOE,DEFAULT_FORCE_ENABLE_NANOE
14 | from .coordinator import PanasonicDeviceCoordinator
15 | from .base import PanasonicDataEntity
16 |
17 | _LOGGER = logging.getLogger(__name__)
18 |
19 | @dataclass(frozen=True, kw_only=True)
20 | class PanasonicSwitchEntityDescription(SwitchEntityDescription):
21 | """Describes Panasonic Switch entity."""
22 |
23 | on_func: Callable[[ChangeRequestBuilder], ChangeRequestBuilder]
24 | off_func: Callable[[ChangeRequestBuilder], ChangeRequestBuilder]
25 | get_state: Callable[[PanasonicDevice], bool]
26 | is_available: Callable[[PanasonicDevice], bool]
27 |
28 |
29 | NANOE_DESCRIPTION = PanasonicSwitchEntityDescription(
30 | key="nanoe",
31 | translation_key="nanoe",
32 | name="Nanoe",
33 | icon="mdi:virus-off",
34 | on_func = lambda builder: builder.set_nanoe_mode(constants.NanoeMode.On),
35 | off_func= lambda builder: builder.set_nanoe_mode(constants.NanoeMode.Off),
36 | get_state = lambda device: device.parameters.nanoe_mode in [constants.NanoeMode.On, constants.NanoeMode.ModeG, constants.NanoeMode.All],
37 | is_available = lambda device: device.has_nanoe
38 | )
39 | ECONAVI_DESCRIPTION = PanasonicSwitchEntityDescription(
40 | key="eco-navi",
41 | translation_key="eco-navi",
42 | name="ECONAVI",
43 | icon="mdi:leaf",
44 | on_func = lambda builder: builder.set_eco_navi_mode(constants.EcoNaviMode.On),
45 | off_func= lambda builder: builder.set_eco_navi_mode(constants.EcoNaviMode.Off),
46 | get_state = lambda device: device.parameters.eco_navi_mode == constants.EcoNaviMode.On,
47 | is_available = lambda device: device.has_eco_navi
48 | )
49 | ECO_FUNCTION_DESCRIPTION = PanasonicSwitchEntityDescription(
50 | key="eco-function",
51 | translation_key="eco-function",
52 | name="AI ECO",
53 | icon="mdi:leaf",
54 | on_func = lambda builder: builder.set_eco_function_mode(constants.EcoFunctionMode.On),
55 | off_func= lambda builder: builder.set_eco_function_mode(constants.EcoFunctionMode.Off),
56 | get_state = lambda device: device.parameters.eco_function_mode == constants.EcoFunctionMode.On,
57 | is_available = lambda device: device.has_eco_function
58 | )
59 | IAUTOX_DESCRIPTION = PanasonicSwitchEntityDescription(
60 | key="iauto-x",
61 | translation_key="iauto-x",
62 | name="iAUTO-X",
63 | icon="mdi:snowflake",
64 | on_func = lambda builder: builder.set_iautox_mode(constants.IAutoXMode.On),
65 | off_func= lambda builder: builder.set_iautox_mode(constants.IAutoXMode.Off),
66 | get_state = lambda device: device.parameters.iautox_mode == constants.IAutoXMode.On and device.parameters.mode == constants.OperationMode.Auto,
67 | is_available = lambda device: device.has_iauto_x
68 | )
69 |
70 | def create_zone_mode_description(zone: PanasonicDeviceZone):
71 | return PanasonicSwitchEntityDescription(
72 | key = f"zone-{zone.id}",
73 | translation_key=f"zone-{zone.id}",
74 | name = zone.name,
75 | icon="mdi:thermostat",
76 | off_func=lambda builder: builder.set_zone_mode(zone.id, constants.ZoneMode.Off),
77 | on_func=lambda builder: builder.set_zone_mode(zone.id, constants.ZoneMode.On),
78 | get_state=lambda device: device.parameters.get_zone(zone.id).mode == constants.ZoneMode.On,
79 | is_available=lambda device: True
80 | )
81 |
82 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities):
83 | devices = []
84 | data_coordinators: list[PanasonicDeviceCoordinator] = hass.data[DOMAIN][DATA_COORDINATORS]
85 | force_enable_nanoe = entry.options.get(CONF_FORCE_ENABLE_NANOE, DEFAULT_FORCE_ENABLE_NANOE)
86 | for data_coordinator in data_coordinators:
87 | devices.append(PanasonicSwitchEntity(data_coordinator, NANOE_DESCRIPTION, always_available=force_enable_nanoe))
88 | devices.append(PanasonicSwitchEntity(data_coordinator, ECONAVI_DESCRIPTION))
89 | devices.append(PanasonicSwitchEntity(data_coordinator, ECO_FUNCTION_DESCRIPTION))
90 | devices.append(PanasonicSwitchEntity(data_coordinator, IAUTOX_DESCRIPTION))
91 | if data_coordinator.device.has_zones:
92 | for zone in data_coordinator.device.parameters.zones:
93 | devices.append(PanasonicSwitchEntity(
94 | data_coordinator,
95 | create_zone_mode_description(zone)))
96 |
97 | async_add_entities(devices)
98 |
99 | class PanasonicSwitchEntityBase(SwitchEntity):
100 | """Base class for all Panasonic switch entities."""
101 |
102 | _attr_device_class = SwitchDeviceClass.SWITCH
103 | entity_description: PanasonicSwitchEntityDescription # type: ignore[override]
104 |
105 | class PanasonicSwitchEntity(PanasonicDataEntity, PanasonicSwitchEntityBase):
106 | """Representation of a Panasonic switch."""
107 |
108 | def __init__(self, coordinator: PanasonicDeviceCoordinator, description: PanasonicSwitchEntityDescription, always_available: bool = False):
109 | """Initialize the Switch."""
110 | self.entity_description = description
111 | self._always_available = always_available
112 | super().__init__(coordinator, description.key)
113 |
114 | @property
115 | def available(self) -> bool:
116 | """Return if entity is available."""
117 | return self._always_available or self.entity_description.is_available(self.coordinator.device)
118 |
119 | def _async_update_attrs(self) -> None:
120 | """Update the attributes of the sensor."""
121 | self._attr_available = self.entity_description.is_available(self.coordinator.device)
122 | self._attr_is_on = self.entity_description.get_state(self.coordinator.device)
123 |
124 |
125 | async def async_turn_on(self, **kwargs):
126 | """Turn on the Switch."""
127 | builder = self.coordinator.get_change_request_builder()
128 | self.entity_description.on_func(builder)
129 | await self.coordinator.async_apply_changes(builder)
130 | self._attr_is_on = True
131 | self.async_write_ha_state()
132 |
133 | async def async_turn_off(self, **kwargs):
134 | """Turn off the Switch."""
135 | builder = self.coordinator.get_change_request_builder()
136 | self.entity_description.off_func(builder)
137 | await self.coordinator.async_apply_changes(builder)
138 | self._attr_is_on = False
139 | self.async_write_ha_state()
140 |
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/translations/cs.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "step": {
4 | "user": {
5 | "title": "Panasonic Comfort Cloud",
6 | "description": "Zadejte vaše Panasonic ID a heslo",
7 | "data": {
8 | "username": "Panasonic ID",
9 | "password": "Heslo",
10 | "enable_daily_energy_sensor": "Povolit měření denní energie",
11 | "force_enable_nanoe": "Povolit přepínač Nanoe pro všechna zařízení",
12 | "use_panasonic_preset_names": "Použít režimy 'Tichý' a 'Výkonný' místo 'Eco' a 'Boost'",
13 | "device_fetch_interval": "Prodleva vyčítání zařízení (sekunda)",
14 | "energy_fetch_interval": "Prodleva vyčítání energie (sekunda)"
15 | }
16 | },
17 | "reconfigure_confirm": {
18 | "title": "Překonfigurace Panasonic Comfort Cloud",
19 | "description": "Zadejte vaše Panasonic ID a heslo pro opětovné ověření",
20 | "data": {
21 | "username": "Panasonic ID",
22 | "password": "Heslo"
23 | }
24 | }
25 | },
26 | "error": {
27 | "no_devices": "Nebyla nalezena jákákoliv zařízení. Prosíme zkontrolujte váš účet a aplikaci CFC a zkuste to znovu.",
28 | "device_timeout": "Vypršel čas pro připojení k API.",
29 | "device_fail": "Neočekávaná chyba při připojování k API.",
30 | "invalid_user_password": "Neplatný Panasonic ID nebo heslo."
31 | },
32 | "abort": {
33 | "device_timeout": "Vypršel čas pro připojení k zařízení.",
34 | "device_fail": "Neočekávaná chyba při vytváření zařízení.",
35 | "already_configured": "Zařízení je již nastavené",
36 | "reauth_successful": "Opětovné ověření bylo úspěšné."
37 | }
38 | },
39 | "options": {
40 | "step": {
41 | "init": {
42 | "data": {
43 | "force_outside_sensor": "Vynutit povolení venkovního teploměru",
44 | "enable_daily_energy_sensor": "Povolit měření denní energie (vyžaduje restart)",
45 | "force_enable_nanoe": "Povolit přepínač Nanoe pro všechna zařízení (vyžaduje restart)",
46 | "use_panasonic_preset_names": "Použít režimy 'Tichý' a 'Výkonný' místo 'Eco' a 'Boost' (vyžaduje restart)",
47 | "device_fetch_interval": "Prodleva vyčítání zařízení (sekunda)",
48 | "energy_fetch_interval": "Prodleva vyčítání energie (sekunda)"
49 | }
50 | }
51 | }
52 | },
53 | "entity": {
54 | "climate": {
55 | "climate": {
56 | "state_attributes": {
57 | "preset_mode": {
58 | "state": {
59 | "heat_8_15": "+8/15 °C",
60 | "none": "Žádný",
61 | "quiet": "Tichý",
62 | "powerful": "Výkonný"
63 | }
64 | },
65 | "swing_mode": {
66 | "state": {
67 | "Auto": "Auto",
68 | "Up": "Nahoru",
69 | "UpMid": "Středně nahoru",
70 | "Mid": "Střed",
71 | "DownMid": "Středně dolů",
72 | "Down": "Dolů",
73 | "Swing": "Kmitání"
74 | }
75 | },
76 | "swing_horizontal_mode":{
77 | "state": {
78 | "Auto": "Auto",
79 | "Left": "Vlevo",
80 | "LeftMid": "Středně vlevo",
81 | "Mid": "Střed",
82 | "RightMid": "Středně vpravo",
83 | "Right": "Vpravo"
84 | }
85 | },
86 | "fan_mode": {
87 | "state": {
88 | "Auto": "Auto",
89 | "Low": "Nejslabší",
90 | "LowMid": "Slabší",
91 | "Mid": "Střední",
92 | "HighMid": "Silnější",
93 | "High": "Nejsilnější"
94 | }
95 | }
96 | }
97 | }
98 | },
99 | "select": {
100 | "horizontal_swing": {
101 | "state": {
102 | "Auto": "Auto",
103 | "Left": "Vlevo",
104 | "LeftMid": "Středně vlevo",
105 | "Mid": "Střed",
106 | "RightMid": "Středně vpravo",
107 | "Right": "Vpravo"
108 | }
109 | },
110 | "vertical_swing": {
111 | "state": {
112 | "Auto": "Auto",
113 | "Up": "Nahoru",
114 | "UpMid": "Středně nahoru",
115 | "Mid": "Střed",
116 | "DownMid": "Středně dolů",
117 | "Down": "Dolů"
118 | }
119 | }
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/translations/de.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "step": {
4 | "user": {
5 | "title": "Panasonic Comfort Cloud",
6 | "description": "Geben Sie Ihre Panasonic-ID und Ihr Passwort ein",
7 | "data": {
8 | "username": "Panasonic ID",
9 | "password": "Passwort",
10 | "enable_daily_energy_sensor": "Tägliche Energiesensoren freischalten",
11 | "force_enable_nanoe": "Einschalte Nanoe für alle Geräte",
12 | "use_panasonic_preset_names": "'Leise' und 'Stark' anstelle von 'Eco' und 'Boost' verwenden",
13 | "device_fetch_interval": "Abrufintervall für Gerät (Sekunden)",
14 | "energy_fetch_interval": "Abrufintervall für Energie (Sekunden)"
15 | }
16 | }
17 | },
18 | "abort": {
19 | "device_timeout": "Zeitüberschreitung beim Verbinden mit dem Gerät.",
20 | "device_fail": "Unerwarteter Fehler beim Erstellen des Geräts.",
21 | "already_configured": "Gerät ist bereits konfiguriert"
22 | }
23 | },
24 | "options": {
25 | "step": {
26 | "init": {
27 | "data": {
28 | "force_outside_sensor": "Force outside sensor",
29 | "enable_daily_energy_sensor": "Tägliche Energiesensoren freischalten (Neustart erforderlich)",
30 | "force_enable_nanoe": "Einschalte Nanoe für alle GeräteEinschalte Nanoe für alle Geräte (Neustart erforderlich)",
31 | "use_panasonic_preset_names": "'Leise' und 'Stark' anstelle von 'Eco' und 'Boost' verwenden",
32 | "device_fetch_interval": "Abrufintervall für Gerät (Sekunden)",
33 | "energy_fetch_interval": "Abrufintervall für Energie (Sekunden)"
34 | }
35 | }
36 | }
37 | },
38 | "entity": {
39 | "climate": {
40 | "climate": {
41 | "state_attributes": {
42 | "preset_mode": {
43 | "state": {
44 | "heat_8_15": "+8/15°C",
45 | "none": "Kein",
46 | "quiet": "Leise",
47 | "powerful": "Stark"
48 | }
49 | },
50 | "swing_mode":{
51 | "state": {
52 | "Auto": "Auto",
53 | "Up": "Oben",
54 | "UpMid": "Oben-Mitte",
55 | "Mid": "Mitte",
56 | "DownMid": "Unten-Mitte",
57 | "Down": "Unten",
58 | "Swing": "Schwingen"
59 | }
60 | },
61 | "swing_horizontal_mode":{
62 | "state": {
63 | "Auto": "Auto",
64 | "Left": "Links",
65 | "LeftMid": "Mitte-Links",
66 | "Mid": "Mitte",
67 | "RightMid": "Mitte-Rechts",
68 | "Right": "Rechts"
69 | }
70 | },
71 | "fan_mode":{
72 | "state": {
73 | "Low": "Niedrig",
74 | "LowMid": "Niedrig-Mittel",
75 | "Mid": "Mittel",
76 | "HighMid": "Mittel-Hoch",
77 | "High": "Hoch"
78 | }
79 | }
80 | }
81 | }
82 | },
83 | "select": {
84 | "horizontal_swing":{
85 | "state": {
86 | "Auto": "Auto",
87 | "Left": "Links",
88 | "LeftMid": "Mitte-Links",
89 | "Mid": "Mitte",
90 | "RightMid": "Mitte-Rechts",
91 | "Right": "Rechts"
92 | }
93 | },
94 | "vertical_swing":{
95 | "state": {
96 | "Auto": "Auto",
97 | "Up": "Oben",
98 | "UpMid": "Oben-Mitte",
99 | "Mid": "Mitte",
100 | "DownMid": "Unten-Mitte",
101 | "Down": "Unten"
102 | }
103 | }
104 | }
105 | }
106 | }
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/translations/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "step": {
4 | "user": {
5 | "title": "Panasonic Comfort Cloud",
6 | "description": "Enter your Panasonic ID and password",
7 | "data": {
8 | "username": "Panasonic ID",
9 | "password": "Password",
10 | "enable_daily_energy_sensor": "Enable daily energy sensors",
11 | "force_enable_nanoe": "Enable Nanoe switch for all devices",
12 | "use_panasonic_preset_names": "Use 'Quiet' and 'Powerful' instead of 'Eco' and 'Boost' Presets",
13 | "device_fetch_interval": "Device fetch interval (seconds)",
14 | "energy_fetch_interval": "Energy fetch interval (seconds)"
15 | }
16 | },
17 | "reconfigure_confirm": {
18 | "title": "Reconfigure Panasonic Comfort Cloud",
19 | "description": "Enter your Panasonic ID and password to re-authenticate",
20 | "data": {
21 | "username": "Panasonic ID",
22 | "password": "Password"
23 | }
24 | }
25 | },
26 | "error":{
27 | "no_devices": "No devices found. Please check your the account and CFC app and try again.",
28 | "device_timeout": "Timeout connecting to the API.",
29 | "device_fail": "Unexpected error connecting to the API.",
30 | "invalid_user_password": "Invalid Panasonic ID or password."
31 | },
32 | "abort": {
33 | "device_timeout": "Timeout connecting to the device.",
34 | "device_fail": "Unexpected error creating device.",
35 | "already_configured": "Device is already configured",
36 | "reauth_successful": "Re-authentication successful."
37 | }
38 | },
39 | "options": {
40 | "step": {
41 | "init": {
42 | "data": {
43 | "force_outside_sensor": "Force outside sensor",
44 | "enable_daily_energy_sensor": "Enable daily energy sensors (requires restart)",
45 | "force_enable_nanoe": "Enable Nanoe switch for all devices (requires restart)",
46 | "use_panasonic_preset_names": "Use 'Quiet' and 'Powerful' instead of 'Eco' and 'Boost' Presets (requires restart)",
47 | "device_fetch_interval": "Device fetch interval (seconds)",
48 | "energy_fetch_interval": "Energy fetch interval (seconds)"
49 | }
50 | }
51 | }
52 | },
53 | "entity": {
54 | "climate": {
55 | "climate": {
56 | "state_attributes": {
57 | "preset_mode": {
58 | "state": {
59 | "heat_8_15": "+8/15°C",
60 | "none": "None",
61 | "quiet": "Quiet",
62 | "powerful": "Powerful"
63 | }
64 | },
65 | "swing_mode":{
66 | "state": {
67 | "Auto": "Auto",
68 | "Up": "Top",
69 | "UpMid": "Mid-top",
70 | "Mid": "Middle",
71 | "DownMid": "Mid-bottom",
72 | "Down": "Bottom",
73 | "Swing": "Swing"
74 | }
75 | },
76 | "swing_horizontal_mode":{
77 | "state": {
78 | "Auto": "Auto",
79 | "Left": "Left",
80 | "LeftMid": "Center left",
81 | "Mid": "Center",
82 | "RightMid": "Center right",
83 | "Right": "Right"
84 | }
85 | },
86 | "fan_mode":{
87 | "state": {
88 | "Auto": "Auto",
89 | "Low": "Low",
90 | "LowMid": "Medium-Low",
91 | "Mid": "Medium",
92 | "HighMid": "Medium-High",
93 | "High": "High"
94 | }
95 | }
96 | }
97 | }
98 | },
99 | "select": {
100 | "horizontal_swing":{
101 | "state": {
102 | "Auto": "Auto",
103 | "Left": "Left",
104 | "LeftMid": "Center left",
105 | "Mid": "Center",
106 | "RightMid": "Center right",
107 | "Right": "Right"
108 | }
109 | },
110 | "vertical_swing":{
111 | "state": {
112 | "Auto": "Auto",
113 | "Up": "Top",
114 | "UpMid": "Mid-top",
115 | "Mid": "Middle",
116 | "DownMid": "Mid-bottom",
117 | "Down": "Bottom"
118 | }
119 | }
120 | }
121 | },
122 | "services": {
123 | "set_horizontal_swing_mode": {
124 | "name": "Set the horizontal swing mode",
125 | "description": "Set the horizontal swing mode for climate device.",
126 | "fields": {
127 | "entity_id": {
128 | "name": "Entity id",
129 | "description": "Name(s) of entities to change."
130 | },
131 | "swing_mode": {
132 | "name": "Swing mode",
133 | "description": "New horizontal swing mode."
134 | }
135 | }
136 | }
137 | }
138 | }
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/translations/it.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "step": {
4 | "user": {
5 | "title": "Panasonic Comfort Cloud",
6 | "description": "Inserisci il tuo ID Panasonic e password",
7 | "data": {
8 | "username": "ID Panasonic",
9 | "password": "Password",
10 | "enable_daily_energy_sensor": "Abilita tutti i sensori di energia giornalieri.",
11 | "force_enable_nanoe": "Ablita Nanoe per tutti i dispositivi.",
12 | "use_panasonic_preset_names": "Usa i profili 'Quiet' e 'Powerful' invece di 'Eco' e 'Boost'",
13 | "device_fetch_interval": "Intervallo interrogazione dispositivo (secondi)",
14 | "energy_fetch_interval": "Intervallo interrogazione energia (secondi)"
15 | }
16 | },
17 | "reconfigure_confirm": {
18 | "title": "Riconfigura Panasonic Comfort Cloud",
19 | "description": "Inserisci il tuo ID Panasonic e password per riconnettersi",
20 | "data": {
21 | "username": "ID Panasonic",
22 | "password": "Password"
23 | }
24 | }
25 | },
26 | "error":{
27 | "no_devices": "Nessun dispositivo trovato. Controlla il tuo account e l'app CFC e riprova.",
28 | "device_timeout": "Timeout scaduto connessione alle API.",
29 | "device_fail": "Errore inatteso connessione alle API.",
30 | "invalid_user_password": "ID Panasonic o password non corretta."
31 | },
32 | "abort": {
33 | "device_timeout": "Timeout connessione al dispositivo.",
34 | "device_fail": "Errore inatteso durante la creazione del dispositivo.",
35 | "already_configured": "Dispositivo già configurato.",
36 | "reauth_successful": "Riconnessione avvenuta con successo."
37 | }
38 | },
39 | "options": {
40 | "step": {
41 | "init": {
42 | "data": {
43 | "force_outside_sensor": "Forza sensore esterno",
44 | "enable_daily_energy_sensor": "Abilita i sensori energia giornalieri (riavvio necessario)",
45 | "force_enable_nanoe": "Abilita Nonoe per tutti i dispositivi (riavvio necessario)",
46 | "use_panasonic_preset_names": "Usa i profili 'Quiet' e 'Powerful' invece di 'Eco' e 'Boost' (riavvio necessario)",
47 | "device_fetch_interval": "Intervallo interrogazione dispositivo (secondi)",
48 | "energy_fetch_interval": "Intervallo interrogazione energia (secondi)"
49 | }
50 | }
51 | }
52 | },
53 | "entity": {
54 | "climate": {
55 | "climate": {
56 | "state_attributes": {
57 | "preset_mode": {
58 | "state": {
59 | "heat_8_15": "+8/15°C",
60 | "none": "Nessuno",
61 | "quiet": "Quiet",
62 | "powerful": "Powerful"
63 | }
64 | },
65 | "swing_mode":{
66 | "state": {
67 | "Auto": "Automatico",
68 | "Up": "Alto",
69 | "UpMid": "Medio-alto",
70 | "Mid": "Medio",
71 | "DownMid": "Medio-basso",
72 | "Down": "Basso",
73 | "Swing": "Oscilla"
74 | }
75 | },
76 | "swing_horizontal_mode":{
77 | "state": {
78 | "Auto": "Automatico",
79 | "Left": "Sinistra",
80 | "LeftMid": "Centro sinistra",
81 | "Mid": "Centro",
82 | "RightMid": "Centro destra",
83 | "Right": "Destra"
84 | }
85 | },
86 | "fan_mode":{
87 | "state": {
88 | "Auto": "Automatica",
89 | "Low": "Bassa",
90 | "LowMid": "Medio-Bassa",
91 | "Mid": "Media",
92 | "HighMid": "Medio-Alta",
93 | "High": "Alta"
94 | }
95 | }
96 | }
97 | }
98 | },
99 | "select": {
100 | "horizontal_swing":{
101 | "state": {
102 | "Auto": "Automatico",
103 | "Left": "Sinistra",
104 | "LeftMid": "Centro sinistra",
105 | "Mid": "Centro",
106 | "RightMid": "Centro destra",
107 | "Right": "Destra"
108 | }
109 | },
110 | "vertical_swing":{
111 | "state": {
112 | "Auto": "Automatico",
113 | "Up": "Alto",
114 | "UpMid": "Medio-alto",
115 | "Mid": "Medio",
116 | "DownMid": "Medio-basso",
117 | "Down": "Basso"
118 | }
119 | }
120 | }
121 | }
122 | }
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/translations/nb.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "step": {
4 | "user": {
5 | "title": "Panasonic Comfort Cloud",
6 | "description": "Skriv inn Panasonic ID og passord",
7 | "data": { "username": "Panasonic ID", "password": "Passord" }
8 | }
9 | },
10 | "abort": {
11 | "device_timeout": "Tidsavbrudd for tilkobling til enheten.",
12 | "device_fail": "Uventet feil under oppretting av enhet.",
13 | "already_configured": "Enheten er allerede konfigurert"
14 | }
15 | },
16 | "options": {
17 | "step": {
18 | "init": {
19 | "data": {
20 | "force_outside_sensor": "Tving utvendig sensor",
21 | "enable_daily_energy_sensor": "Aktiver daglig energisensor (krever omstart)"
22 | }
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/translations/pl.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "step": {
4 | "user": {
5 | "title": "Panasonic Comfort Cloud",
6 | "description": "Wprowadź swoje Panasonic ID i hasło",
7 | "data": {
8 | "username": "Panasonic ID",
9 | "password": "Hasło",
10 | "enable_daily_energy_sensor": "Włącz dzienne czujniki energii",
11 | "force_enable_nanoe": "Włącz przełącznik Nanoe dla wszystkich urządzeń",
12 | "use_panasonic_preset_names": "Używaj 'Cichy' i 'Mocny' zamiast 'Eco' i 'Boost'",
13 | "device_fetch_interval": "Interwał pobierania urządzenia (sekundy)",
14 | "energy_fetch_interval": "Interwał pobierania energii (sekundy)"
15 | }
16 | },
17 | "reconfigure_confirm": {
18 | "title": "Ponowna konfiguracja Panasonic Comfort Cloud",
19 | "description": "Wprowadź swoje Panasonic ID i hasło, aby ponownie uwierzytelnić",
20 | "data": {
21 | "username": "Panasonic ID",
22 | "password": "Hasło"
23 | }
24 | }
25 | },
26 | "error": {
27 | "no_devices": "Nie znaleziono urządzeń. Sprawdź konto i aplikację CFC, a następnie spróbuj ponownie.",
28 | "device_timeout": "Przekroczono czas oczekiwania na połączenie z API.",
29 | "device_fail": "Nieoczekiwany błąd podczas połączenia z API.",
30 | "invalid_user_password": "Nieprawidłowe Panasonic ID lub hasło."
31 | },
32 | "abort": {
33 | "device_timeout": "Przekroczono czas oczekiwania na połączenie z urządzeniem.",
34 | "device_fail": "Nieoczekiwany błąd podczas tworzenia urządzenia.",
35 | "already_configured": "Urządzenie jest już skonfigurowane.",
36 | "reauth_successful": "Ponowne uwierzytelnienie zakończone sukcesem."
37 | }
38 | },
39 | "options": {
40 | "step": {
41 | "init": {
42 | "data": {
43 | "force_outside_sensor": "Wymuś czujnik zewnętrzny",
44 | "enable_daily_energy_sensor": "Włącz dzienne czujniki energii (wymaga ponownego uruchomienia)",
45 | "force_enable_nanoe": "Włącz przełącznik Nanoe dla wszystkich urządzeń (wymaga ponownego uruchomienia)",
46 | "use_panasonic_preset_names": "Używaj 'Cichy' i 'Mocny' zamiast 'Eco' i 'Boost' (wymaga ponownego uruchomienia)",
47 | "device_fetch_interval": "Interwał pobierania urządzenia (sekundy)",
48 | "energy_fetch_interval": "Interwał pobierania energii (sekundy)"
49 | }
50 | }
51 | }
52 | },
53 | "entity": {
54 | "climate": {
55 | "climate": {
56 | "state_attributes": {
57 | "preset_mode": {
58 | "state": {
59 | "heat_8_15": "+8/15°C",
60 | "none": "Brak",
61 | "quiet": "Cichy",
62 | "powerful": "Mocny"
63 | }
64 | },
65 | "swing_mode": {
66 | "state": {
67 | "Auto": "Auto",
68 | "Up": "Wysoki",
69 | "UpMid": "Średniowysoki",
70 | "Mid": "Średni",
71 | "DownMid": "Średnioniski",
72 | "Down": "Niski",
73 | "Swing": "Wahadłowy"
74 | }
75 | },
76 | "swing_horizontal_mode": {
77 | "state": {
78 | "Auto": "Auto",
79 | "Left": "Lewo",
80 | "LeftMid": "Lewy środek",
81 | "Mid": "Środek",
82 | "RightMid": "Prawy środek",
83 | "Right": "Prawo"
84 | }
85 | },
86 | "fan_mode": {
87 | "state": {
88 | "Auto": "Auto",
89 | "Low": "Niski",
90 | "LowMid": "Średnioniski",
91 | "Mid": "Średni",
92 | "HighMid": "Średniowysoki",
93 | "High": "Wysoki"
94 | }
95 | }
96 | }
97 | }
98 | },
99 | "select": {
100 | "horizontal_swing": {
101 | "state": {
102 | "Auto": "Auto",
103 | "Left": "Lewo",
104 | "LeftMid": "Lewy środek",
105 | "Mid": "Środek",
106 | "RightMid": "Prawy środek",
107 | "Right": "Prawo"
108 | }
109 | },
110 | "vertical_swing": {
111 | "state": {
112 | "Auto": "Auto",
113 | "Up": "Wysoki",
114 | "UpMid": "Średniowysoki",
115 | "Mid": "Średni",
116 | "DownMid": "Średnioniski",
117 | "Down": "Niski"
118 | }
119 | }
120 | }
121 | },
122 | "services": {
123 | "set_horizontal_swing_mode": {
124 | "name": "Ustaw poziomy kierunek nawiewu",
125 | "description": "Ustaw poziomy kierunek nawiewu dla urządzenia klimatyzacyjnego.",
126 | "fields": {
127 | "entity_id": {
128 | "name": "Identyfikator urządzenia",
129 | "description": "Nazwa urządzenia, dla którego ma zostać zmieniony kierunek nawiewu."
130 | },
131 | "swing_mode": {
132 | "name": "Kierunek nawiewu",
133 | "description": "Nowy poziomy kierunek nawiewu."
134 | }
135 | }
136 | }
137 | }
138 | }
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/translations/se.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "step": {
4 | "user": {
5 | "title": "Panasonic Comfort Cloud",
6 | "description": "Fyll i ditt Panasonic ID och lösenord",
7 | "data": {
8 | "username": "Panasonic ID",
9 | "password": "Lösenord",
10 | "enable_daily_energy_sensor": "Aktivera dagliga energisensorer",
11 | "force_enable_nanoe": "Aktivera Nanoe switch för alla enheter",
12 | "use_panasonic_preset_names": "Använd 'Tyst' och 'Kraftfull' istället för 'Eco' och 'Boost' läge",
13 | "device_fetch_interval": "Enhetshämtningsintervall (sekunder)",
14 | "energy_fetch_interval": "Energihämtningsintervall (sekunder)"
15 | }
16 | }
17 | },
18 | "error":{
19 | "no_devices": "Ingen enheter hittades. Kontrollera ditt Panasonic Konto i CFC Appen och försök igen.",
20 | "device_timeout": "Timeout för anslutning till API:et.",
21 | "device_fail": "Oväntat fel vid anslutning till API:et.",
22 | "invalid_user_password": "Felaktigt Panasonic ID eller lösenord."
23 | },
24 | "abort": {
25 | "device_timeout": "Timeout för anslutning till enheten.",
26 | "device_fail": "Oväntat fel när enheten skapas.",
27 | "already_configured": "Enheten är redan konfigurerad.",
28 | "reauth_successful": "Återautentisering lyckades."
29 | }
30 | },
31 | "options": {
32 | "step": {
33 | "init": {
34 | "data": {
35 | "force_outside_sensor": "Forcera yttre sensor",
36 | "enable_daily_energy_sensor": "Aktivera dagliga energisensorer (kräver omstart)",
37 | "force_enable_nanoe": "Aktivera Nanoe switch för alla enheter (kräver omstart)",
38 | "use_panasonic_preset_names": "Använd 'Tyst' och 'Kraftfull' istället för 'Eco' och 'Boost' läge (kräver omstart)",
39 | "device_fetch_interval": "Enhetshämtningsintervall (sekunder)",
40 | "energy_fetch_interval": "Energihämtningsintervall (sekunder)"
41 | }
42 | }
43 | }
44 | },
45 | "entity": {
46 | "climate": {
47 | "climate": {
48 | "state_attributes": {
49 | "preset_mode": {
50 | "state": {
51 | "heat_8_15": "+8/15°C",
52 | "none": "Standard",
53 | "quiet": "Tyst",
54 | "powerful": "Kraftfull"
55 | }
56 | },
57 | "swing_mode":{
58 | "state": {
59 | "Auto": "Auto",
60 | "Up": "Upp",
61 | "UpMid": "Mitt-upp",
62 | "Mid": "Mitten",
63 | "DownMid": "Mitt-Neråt",
64 | "Down": "Neråt",
65 | "Swing": "Svängande"
66 | }
67 | },
68 | "swing_horizontal_mode":{
69 | "state": {
70 | "Auto": "Auto",
71 | "Left": "Vänster",
72 | "LeftMid": "Mitten vänster",
73 | "Mid": "Mitten",
74 | "RightMid": "Mitten höger",
75 | "Right": "Höger"
76 | }
77 | },
78 | "fan_mode":{
79 | "state": {
80 | "Auto": "Auto",
81 | "Low": "Låg",
82 | "LowMid": "Medium-Låg",
83 | "Mid": "Medium",
84 | "HighMid": "Medium-Hög",
85 | "High": "Hög"
86 | }
87 | }
88 | }
89 | }
90 | },
91 | "select": {
92 | "horizontal_swing":{
93 | "state": {
94 | "Auto": "Auto",
95 | "Left": "Vänster",
96 | "LeftMid": "Mitten vänster",
97 | "Mid": "Mitten",
98 | "RightMid": "Mitten höger",
99 | "Right": "Höger"
100 | }
101 | },
102 | "vertical_swing":{
103 | "state": {
104 | "Auto": "Auto",
105 | "Up": "Upp",
106 | "UpMid": "Mitt-upp",
107 | "Mid": "Mitten",
108 | "DownMid": "Mitt-Neråt",
109 | "Down": "Neråt"
110 | }
111 | }
112 | }
113 | }
114 | }
--------------------------------------------------------------------------------
/custom_components/panasonic_cc/water_heater.py:
--------------------------------------------------------------------------------
1 | """Support for the Aquarea Tank."""
2 | import logging
3 | from dataclasses import dataclass
4 |
5 | from homeassistant.core import HomeAssistant
6 | from homeassistant.config_entries import ConfigEntry
7 | from homeassistant.const import UnitOfTemperature, STATE_OFF, STATE_IDLE, PRECISION_WHOLE, ATTR_TEMPERATURE, MAJOR_VERSION
8 | from homeassistant.components.water_heater import (
9 | STATE_HEAT_PUMP,
10 | WaterHeaterEntity,
11 | WaterHeaterEntityFeature
12 | )
13 | if MAJOR_VERSION >= 2025:
14 | from homeassistant.components.water_heater import WaterHeaterEntityDescription
15 | else:
16 | from homeassistant.components.water_heater import WaterHeaterEntityEntityDescription as WaterHeaterEntityDescription
17 |
18 | from .base import AquareaDataEntity
19 | from .coordinator import AquareaDeviceCoordinator
20 | from .const import STATE_HEATING
21 | from aioaquarea.data import DeviceAction, OperationStatus
22 |
23 | from .const import (
24 | DOMAIN,
25 | AQUAREA_COORDINATORS)
26 |
27 | _LOGGER = logging.getLogger(__name__)
28 |
29 | @dataclass(frozen=True, kw_only=True)
30 | class AquareaWaterHeaterEntityDescription(WaterHeaterEntityDescription):
31 | """Describes a Aquarea Water Heater entity."""
32 |
33 | AQUAREA_WATER_TANK_DESCRIPTION = AquareaWaterHeaterEntityDescription(
34 | key="tank",
35 | translation_key="tank",
36 | name="Tank"
37 | )
38 |
39 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities):
40 | entities = []
41 | aquarea_coordinators: list[AquareaDeviceCoordinator] = hass.data[DOMAIN][AQUAREA_COORDINATORS]
42 | for aquarea_coordinator in aquarea_coordinators:
43 | if aquarea_coordinator.device.tank is None:
44 | continue
45 | entities.append(AquareaWaterHeater(aquarea_coordinator, AQUAREA_WATER_TANK_DESCRIPTION))
46 | async_add_entities(entities)
47 |
48 | class AquareaWaterHeater(AquareaDataEntity, WaterHeaterEntity):
49 | """Representation of a Aquarea Water Tank."""
50 |
51 | _attr_temperature_unit = UnitOfTemperature.CELSIUS
52 | _attr_supported_features = WaterHeaterEntityFeature.TARGET_TEMPERATURE | WaterHeaterEntityFeature.OPERATION_MODE
53 | _attr_operation_list = [STATE_HEATING, STATE_OFF]
54 | _attr_precision = PRECISION_WHOLE
55 | _attr_target_temperature_step = 1
56 |
57 | def __init__(self, coordinator: AquareaDeviceCoordinator, description: AquareaWaterHeaterEntityDescription):
58 | """Initialize the climate entity."""
59 | self.entity_description = description
60 |
61 | super().__init__(coordinator, description.key)
62 | _LOGGER.info(f"Registing Climate entity: '{self._attr_unique_id}'")
63 |
64 | def _async_update_attrs(self) -> None:
65 | """Update attributes."""
66 | device = self.coordinator.device
67 |
68 | if device.tank is None:
69 | self._attr_available = False
70 | return
71 |
72 | self._attr_min_temp = device.tank.heat_min
73 | self._attr_max_temp = device.tank.heat_max
74 | self._attr_target_temperature = device.tank.target_temperature
75 | self._attr_current_temperature = device.tank.temperature
76 |
77 | if device.tank.operation_status == OperationStatus.OFF:
78 | self._attr_state = STATE_OFF
79 | self._attr_current_operation = STATE_OFF
80 | else:
81 | self._attr_state = STATE_HEAT_PUMP
82 |
83 | self._attr_current_operation = (
84 | STATE_HEATING
85 | if device.current_action == DeviceAction.HEATING_WATER
86 | else STATE_IDLE
87 | )
88 |
89 | async def async_set_temperature(self, **kwargs):
90 | """Set new target temperature."""
91 | temperature: float | None = kwargs.get(ATTR_TEMPERATURE)
92 | if temperature is None:
93 | return
94 | await self.coordinator.device.tank.set_target_temperature(int(temperature))
95 |
96 | async def async_set_operation_mode(self, operation_mode):
97 | if operation_mode == STATE_HEATING:
98 | await self.coordinator.device.tank.turn_on()
99 | elif operation_mode == STATE_OFF:
100 | await self.coordinator.device.tank.turn_off()
--------------------------------------------------------------------------------
/doc/configuration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sockless-coding/panasonic_cc/c9d684c5fd9c5395885f2f2679d4c02f2179a37c/doc/configuration.png
--------------------------------------------------------------------------------
/doc/controls.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sockless-coding/panasonic_cc/c9d684c5fd9c5395885f2f2679d4c02f2179a37c/doc/controls.png
--------------------------------------------------------------------------------
/doc/diagnostics.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sockless-coding/panasonic_cc/c9d684c5fd9c5395885f2f2679d4c02f2179a37c/doc/diagnostics.png
--------------------------------------------------------------------------------
/doc/entities.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sockless-coding/panasonic_cc/c9d684c5fd9c5395885f2f2679d4c02f2179a37c/doc/entities.png
--------------------------------------------------------------------------------
/doc/options_dlg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sockless-coding/panasonic_cc/c9d684c5fd9c5395885f2f2679d4c02f2179a37c/doc/options_dlg.png
--------------------------------------------------------------------------------
/doc/sensors.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sockless-coding/panasonic_cc/c9d684c5fd9c5395885f2f2679d4c02f2179a37c/doc/sensors.png
--------------------------------------------------------------------------------
/doc/setup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sockless-coding/panasonic_cc/c9d684c5fd9c5395885f2f2679d4c02f2179a37c/doc/setup.png
--------------------------------------------------------------------------------
/doc/setup_dlg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sockless-coding/panasonic_cc/c9d684c5fd9c5395885f2f2679d4c02f2179a37c/doc/setup_dlg.png
--------------------------------------------------------------------------------
/hacs.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Panasonic Comfort Cloud",
3 | "homeassistant": "2024.12.1"
4 | }
--------------------------------------------------------------------------------
/info.md:
--------------------------------------------------------------------------------
1 | # Panasonic Comfort Cloud
2 |
3 | This is a custom component to allow control of Panasonic Comfort Cloud devices in [HomeAssistant](https://home-assistant.io).
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | ## IMPORTANT
12 | Before installing this integration, you **must** have **completed** the **2FA** process using the Panasonic Comfort Cloud app.
13 |
14 | # Features:
15 |
16 | * Climate component for Panasonic airconditioners and heatpumps
17 | * Horizontal swing mode selection
18 | * Sensors for inside and outside temperature (where available)
19 | * Switch for toggling Nanoe mode (where available)
20 | * Switch for toggling ECONAVI mode (where available)
21 | * Switch for toggling AI ECO mode (where available)
22 | * Daily energy sensor (optional)
23 | * Current Power sensor (Calculated from energy reading)
24 | * Zone controls (where available)
25 |
26 |
27 | # Configuration
28 |
29 | The Panasonic Comfort Cloud integration can be configured via the Home Assistant integration interface where it will let you enter your Panasonic ID and Password.
30 |
31 | 
32 |
33 | After inital setup, the following options are available:
34 |
35 | 
36 |
37 | #### Support Development
38 | - :coffee: [Buy me a coffee](https://www.buymeacoffee.com/sockless)
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | aiohttp
2 | aio-panasonic-comfort-cloud==2025.5.1
3 | aioaquarea==0.7.2
--------------------------------------------------------------------------------