├── .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 | [![Open your Home Assistant instance and show the system information.](https://my.home-assistant.io/badges/system_health.svg)](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 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) 6 | [![Integration Usage](https://img.shields.io/badge/dynamic/json?color=41BDF5&style=for-the-badge&logo=home-assistant&label=usage&suffix=%20installs&cacheSeconds=15600&url=https://analytics.home-assistant.io/custom_integrations.json&query=$.panasonic_cc.total)](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 | Example controls 20 | Example sensors 21 | Example diagnostics 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. [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](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. [![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](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 | ![Setup](https://github.com/sockless-coding/panasonic_cc/raw/master/doc/setup.png) 55 | 56 | After inital setup, the following options are available: 57 | 58 | ![Setup](https://github.com/sockless-coding/panasonic_cc/raw/master/doc/configuration.png) 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 | Example controls 7 | Example sensors 8 | Example diagnostics 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 | ![Setup](https://github.com/sockless-coding/panasonic_cc/raw/master/doc/setup.png) 32 | 33 | After inital setup, the following options are available: 34 | 35 | ![Setup](https://github.com/sockless-coding/panasonic_cc/raw/master/doc/configuration.png) 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 --------------------------------------------------------------------------------