├── custom_components └── panasonic_ems2 │ ├── core │ ├── __init__.py │ ├── exceptions.py │ ├── apis.py │ ├── base.py │ ├── cloud.py │ └── const.py │ ├── manifest.json │ ├── system_health.py │ ├── translations │ ├── en.json │ └── zh-Hant.json │ ├── binary_sensor.py │ ├── number.py │ ├── __init__.py │ ├── select.py │ ├── sensor.py │ ├── switch.py │ ├── humidifier.py │ ├── config_flow.py │ ├── fan.py │ └── climate.py ├── .gitignore ├── hacs.json ├── .github ├── workflows │ ├── validate.yaml │ └── ossar-analysis.yml └── FUNDING.yml ├── README_zh-tw.md ├── README.md ├── scripts └── panasonic_ems2.py └── LICENSE /custom_components/panasonic_ems2/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _test 2 | __pycache__ 3 | secrets.py 4 | *.pyc 5 | .DS_Store 6 | .vscode -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Panasonic Smart IoT", 3 | "homeassistant": "2023.11.0", 4 | "render_readme": true, 5 | "domain": "panasonic_ems2", 6 | "documentation": "https://github.com/tsunglung/panasonic_ems2", 7 | "issue_tracker": "https://github.com/tsunglung/panasonic_ems2/issues" 8 | } 9 | -------------------------------------------------------------------------------- /custom_components/panasonic_ems2/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "panasonic_ems2", 3 | "name": "Panasonic Smart IoT", 4 | "config_flow": true, 5 | "version": "0.0.1", 6 | "iot_class": "cloud_polling", 7 | "documentation": "https://github.com/tsunglung/panasonic_ems2", 8 | "issue_tracker": "https://github.com/tsunglung/panasonic_ems2/issues", 9 | "codeowners": [ 10 | "@tsunglung" 11 | ] 12 | } -------------------------------------------------------------------------------- /.github/workflows/validate.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | 5 | jobs: 6 | hacs-validate: 7 | name: HACS Validation 8 | runs-on: 'ubuntu-latest' 9 | steps: 10 | - uses: 'actions/checkout@v2' 11 | - name: HACS validation 12 | uses: 'hacs/action@main' 13 | with: 14 | category: 'integration' 15 | hassfest-validate: 16 | name: hassfest Validation 17 | runs-on: 'ubuntu-latest' 18 | steps: 19 | - uses: 'actions/checkout@v2' 20 | - uses: home-assistant/actions/hassfest@master 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 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 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://www.buymeacoffee.com/osk2'] 14 | -------------------------------------------------------------------------------- /custom_components/panasonic_ems2/system_health.py: -------------------------------------------------------------------------------- 1 | """Provide info to system health.""" 2 | 3 | from homeassistant.components import system_health 4 | from homeassistant.core import HomeAssistant, callback 5 | 6 | from .core.const import DOMAIN 7 | 8 | 9 | @callback 10 | def async_register( 11 | hass: HomeAssistant, register: system_health.SystemHealthRegistration 12 | ) -> None: 13 | # pylint: disable=unused-argument 14 | """Register system health callbacks.""" 15 | register.async_register_info(system_health_info) 16 | 17 | 18 | async def system_health_info(hass): 19 | """Get info for the info page.""" 20 | integration = hass.data["integrations"][DOMAIN] 21 | data = {"version": f"{integration.version}"} 22 | data["api_counts"] = hass.data[DOMAIN].get("api_counts", "") 23 | data["api_counts_per_hour"] = hass.data[DOMAIN].get("api_counts_per_hour", "") 24 | 25 | return data -------------------------------------------------------------------------------- /custom_components/panasonic_ems2/core/exceptions.py: -------------------------------------------------------------------------------- 1 | """ Panasonic Smart Home Exceptions.""" 2 | 3 | class Ems2BaseException(Exception): 4 | """ Base exception """ 5 | 6 | 7 | class Ems2TokenNotFound(Ems2BaseException): 8 | """ Refresh token not found """ 9 | 10 | def __init__( 11 | self, message="Refresh token not existed. You may need to open session again." 12 | ): 13 | super().__init__(message) 14 | self.message = message 15 | 16 | 17 | class Ems2TokenExpired(Ems2BaseException): 18 | """ Token expired """ 19 | 20 | 21 | class Ems2InvalidRefreshToken(Ems2BaseException): 22 | """ Refresh token expired """ 23 | 24 | 25 | class Ems2TooManyRequest(Ems2BaseException): 26 | """ Too many request """ 27 | 28 | 29 | class Ems2LoginFailed(Ems2BaseException): 30 | """ Any other login exception """ 31 | 32 | 33 | class Ems2Expectation(Ems2BaseException): 34 | """ Any other exception """ 35 | 36 | 37 | class Ems2ExceedRateLimit(Ems2BaseException): 38 | """ API reaches rate limit """ -------------------------------------------------------------------------------- /custom_components/panasonic_ems2/core/apis.py: -------------------------------------------------------------------------------- 1 | """the Panasonic Smart Home API.""" 2 | 3 | from .const import BASE_URL 4 | 5 | def open_session(): 6 | url = f"{BASE_URL}/userlogin1" 7 | return url 8 | 9 | def close_session(): 10 | url = f"{BASE_URL}/userlogout1" 11 | return url 12 | 13 | def refresh_token(): 14 | url = f"{BASE_URL}/RefreshToken1" 15 | return url 16 | 17 | def get_user_info(): 18 | url = f"{BASE_URL}/UserGetInfo" 19 | return url 20 | 21 | def get_update_info(): 22 | url = "https://ems2.panasonic.com.tw/PSHE_MI/api/S3/UpdateCheck" 23 | return url 24 | 25 | def get_user_devices(): 26 | url = f"{BASE_URL}/UserGetRegisteredGwList2" 27 | return url 28 | 29 | def get_gw_ip(): 30 | url = f"{BASE_URL}/UserGetGWIP" 31 | return url 32 | 33 | def post_device_get_info(): 34 | url = f"{BASE_URL}/DeviceGetInfo" 35 | return url 36 | 37 | def get_device_status(): 38 | url = f"{BASE_URL}/UserGetDeviceStatus" 39 | return url 40 | 41 | def get_plate_mode(): 42 | url = f"{BASE_URL}/PlateGetMode" 43 | return url 44 | 45 | def set_device(): 46 | url = f"{BASE_URL}/DeviceSetCommand" 47 | return url 48 | -------------------------------------------------------------------------------- /custom_components/panasonic_ems2/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Account is already configured", 5 | "single_instance_allowed": "Only a single instance is allowed." 6 | }, 7 | "error": { 8 | "connection_error": "Can't connect to Panasonic Smart Home Cloud.", 9 | "rate_limit": "API rate limit is reached. Please retry later.", 10 | "auth": "Username orPassword is incorrect.", 11 | "network_busy": "Network busy, Please try again later!" 12 | }, 13 | "flow_title": "Panasonic Smart Home: {name}", 14 | "step": { 15 | "user": { 16 | "data": { 17 | "username": "Username", 18 | "password": "Password" 19 | }, 20 | "description": "Login with your Panasonic Smart Home credentials" 21 | } 22 | } 23 | }, 24 | "options": { 25 | "step": { 26 | "init": { 27 | "title": "Panasonic Smart Home", 28 | "data": { 29 | "update_interval": "Password" 30 | } 31 | } 32 | } 33 | }, 34 | "system_health": { 35 | "info": { 36 | "version": "Version", 37 | "api_counts": "Panasonic Cloud API Connections", 38 | "api_counts_per_hour": "Panasonic Cloud API Connections per hour" 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /.github/workflows/ossar-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow integrates a collection of open source static analysis tools 2 | # with GitHub code scanning. For documentation, or to provide feedback, visit 3 | # https://github.com/github/ossar-action 4 | name: OSSAR 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | # The branches below must be a subset of the branches above 11 | branches: [ master ] 12 | schedule: 13 | - cron: '21 5 * * 3' 14 | 15 | jobs: 16 | OSSAR-Scan: 17 | # OSSAR runs on windows-latest. 18 | # ubuntu-latest and macos-latest support coming soon 19 | runs-on: windows-latest 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v2 24 | 25 | # Ensure a compatible version of dotnet is installed. 26 | # The [Microsoft Security Code Analysis CLI](https://aka.ms/mscadocs) is built with dotnet v3.1.201. 27 | # A version greater than or equal to v3.1.201 of dotnet must be installed on the agent in order to run this action. 28 | # GitHub hosted runners already have a compatible version of dotnet installed and this step may be skipped. 29 | # For self-hosted runners, ensure dotnet version 3.1.201 or later is installed by including this action: 30 | # - name: Install .NET 31 | # uses: actions/setup-dotnet@v1 32 | # with: 33 | # dotnet-version: '3.1.x' 34 | 35 | # Run open source static analysis tools 36 | - name: Run OSSAR 37 | uses: github/ossar-action@v1 38 | id: ossar 39 | 40 | # Upload results to the Security tab 41 | - name: Upload OSSAR results 42 | uses: github/codeql-action/upload-sarif@v1 43 | with: 44 | sarif_file: ${{ steps.ossar.outputs.sarifFile }} 45 | -------------------------------------------------------------------------------- /custom_components/panasonic_ems2/translations/zh-Hant.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "\u5E33\u6236\u7d93\u8a2d\u5b9a\u5b8c\u6210", 5 | "single_instance_allowed": "\u50c5\u80fd\u5efa\u7acb\u4e00\u500b\u5be6\u4f8b" 6 | }, 7 | "error": { 8 | "connection_error": "\u7121\u6cd5\u9023\u7dda\u81f3 \u570b\u969b\u724c\u667a\u6167\u64cd\u4f5c\u7cfb\u7d71 \uff0c", 9 | "rate_limit": "API \u5df2\u9054\u7528\u91cf\u4e0a\u9650\uff0c\u8acb\u7a0d\u5019\u91cd\u8a66\u4e00\u6b21", 10 | "auth": "\u5e33\u865f\u6216\u5bc6\u78bc\u6709\u8aa4\u3002", 11 | "network_busy": "\u7db2\u8def\u5fd9\u788c\uff0c\u8acb\u7a0d\u5f8c\u5617\u8a66\uff01!" 12 | }, 13 | "flow_title": "\u570b\u969b\u724c\u667a\u6167\u5bb6\u5ead {name}", 14 | "step": { 15 | "user": { 16 | "data": { 17 | "username": "\u4f7f\u7528\u8005\u5e33\u865f", 18 | "password": "\u5bc6\u78bc" 19 | }, 20 | "description": "\u767b\u5165\u60a8\u7684 \u570b\u969b\u724c\u667a\u6167\u5bb6\u5ead \u5e33\u6236" 21 | } 22 | } 23 | }, 24 | "options": { 25 | "step": { 26 | "init": { 27 | "title": "\u570b\u969b\u724c\u667a\u6167\u64cd\u4f5c\u7cfb\u7d71", 28 | "data": { 29 | "update_interval": "\u66F4\u65B0\u9593\u9694" 30 | } 31 | } 32 | } 33 | }, 34 | "system_health": { 35 | "info": { 36 | "version": "\u7248\u672c", 37 | "api_counts": "Panasonic \u96f2 API \u5b58\u53d6\u6b21\u6578", 38 | "api_counts_per_hour": "\u6bcf\u5c0f\u6642 Panasonic \u96f2 API \u5b58\u53d6\u6b21\u6578" 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /custom_components/panasonic_ems2/core/base.py: -------------------------------------------------------------------------------- 1 | """the Panasonic Smart Home Base Entity.""" 2 | from abc import ABC, abstractmethod 3 | 4 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 5 | from homeassistant.helpers.device_registry import DeviceInfo 6 | 7 | from .const import ( 8 | DOMAIN, 9 | ) 10 | 11 | 12 | class PanasonicBaseEntity(CoordinatorEntity, ABC): 13 | def __init__( 14 | self, 15 | coordinator, 16 | device_gwid, 17 | device_id, 18 | client, 19 | info, 20 | ): 21 | super().__init__(coordinator) 22 | self.client = client 23 | self.device_gwid = device_gwid 24 | self.info = info 25 | self.coordinator = coordinator 26 | 27 | self.device_id = int(device_id) 28 | 29 | @property 30 | def model(self) -> str: 31 | return self.info["Model"] 32 | 33 | @property 34 | def name(self) -> str: 35 | return self.info["NickName"] 36 | 37 | @property 38 | def unique_id(self) -> str: 39 | return self.info["GWID"] 40 | 41 | @property 42 | def device_info(self) -> DeviceInfo: 43 | """Return the device info.""" 44 | return DeviceInfo( 45 | identifiers={(DOMAIN, str(self.device_gwid))}, 46 | # configuration_url="http://{}".format(self.info.get("GWIP", "")), 47 | name=self.info["NickName"], 48 | manufacturer=f"Panasonic {self.info['ModelType']}", 49 | model=self.model, 50 | # sw_version=module.get("firmware_version", ""), 51 | hw_version=self.info["ModelID"] 52 | ) 53 | 54 | @property 55 | def available(self) -> bool: 56 | return True # keep always available 57 | for device in self.info.get("Devices", []): 58 | if self.device_id == device.get("DeviceID", None): 59 | return bool(device["IsAvailable"]) 60 | return False 61 | 62 | def get_status(self, info): 63 | """ 64 | get the status from devices info 65 | """ 66 | if "Information" not in info.get(self.device_gwid, {}): 67 | return {} 68 | for device in info[self.device_gwid]["Information"]: 69 | if self.device_id == device.get("DeviceID", None): 70 | return device["status"] 71 | return {} 72 | -------------------------------------------------------------------------------- /README_zh-tw.md: -------------------------------------------------------------------------------- 1 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) 2 | ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/tsunglung/panasonic_ems2?style=for-the-badge) 3 | [![GitHub license](https://img.shields.io/github/license/tsunglung/panasonic_ems2?style=for-the-badge)](https://github.com/osk2/panasonic_smart_app/blob/master/LICENSE) 4 | 5 | [English](README.md) | [繁體中文](README_zh-tw.md) 6 | 7 | # Panasonic IoT TW 8 | 9 | Home Assistant 的 Panasonic IoT TW [Android](https://play.google.com/store/apps/details?id=com.panasonic.smart&hl=zh_TW&gl=US&pli=1) [iOS](https://apps.apple.com/tw/app/panasonic-iot-tw/id904484053) 整合套件 10 | 11 | 本專案修改自 [Osk2's](https://github.com/osk2) [panasonic_smart_app](https://github.com/osk2/panasonic_smart_appp)。 12 | Buy Me A Coffee 13 | 14 | ## 注意 15 | 16 | 1. 本整合套件僅支援 Panasonic IoT 模組最新版本,請更新 Panasonic IoT 模組的韌體到最新版本。 17 | 2. 這套件全新改寫,歡迎回報。目前已支援空調,洗衣機,冰箱,除溼機,全熱交換機和重量感知板。 18 | 3. 由於 Panasonic IoT 家電有很多型號,每個型號功能都不一樣,如果遇到問題,歡迎回報問題給我修復問題。 19 | 20 | # 安裝 21 | 22 | 你可以用 [HACS](https://hacs.xyz/) 來安裝這個整合。 步驟如下 custom repo: HACS > Integrations > 3 dots (upper top corner) > Custom repositories > URL: `tsunglung/panasonic_ems2` > Category: Integration 23 | 24 | # 手動安裝 25 | 26 | 手動複製 `panasonic_ems2` 資料夾到你的 config 資料夾的 `custom_components` 目錄下。 27 | 28 | 然後重新啟動 Home Assistant. 29 | 30 | # 設定 31 | 32 | **請使用 Home Assistant 整合設定** 33 | 34 | 1. 從 GUI. 設定 > 整合 > 新增 整合 > Panasonic Smart IoT 35 | 1. 如果 `Panasonic Smart IoT` 沒有出現在清單裡,請 重新整理 (REFRESH) 網頁。 36 | 2. 如果 `Panasonic Smart IoT` 還是沒有出現在清單裡,請清除瀏覽器的快取 (Cache)。 37 | 2. 輸入登入資訊 ([Panasonic Cloud](https://club.panasonic.tw/) 的電子郵件及密碼) 38 | 3. 開始使用。 39 | 40 | # 協助加入你的 國際版 IoT 智慧家電到 這個自訂整合 41 | 42 | 如果,你發現你加入整合後,你的智慧家電在 HA 沒有出現或是有實體不正常。很有可能你的國際版 IoT 智慧家電還沒有被支援完整。 43 | 你可以協助改善這整合,只要簡單幾個步驟把你的國際版 IoT 智慧家電 家電資訊寄給我除錯。 44 | 45 | **方法** 46 | 47 | 1. 下載並安裝 [Python](https://www.python.org/downloads/) 48 | 2. 下載腳本 [panasonic_ems2.py](https://github.com/tsunglung/panasonic_ems2/raw/master/scripts/panasonic_ems2.py) 到你的 Windows 或 MacOS 49 | 3. 找到下載的腳本, 並使用 Windows 的 CMD 或是 macOS 的 Terminal, 切換目錄到下載的路徑 "cd [your Download Path]" 50 | 4. 執行下載的指令並登入你的 Panasonic Cloud 帳號 51 | ``` 52 | python panasonic_ems2.py 53 | ``` 54 | 5. 如果登入成功,會有二個檔案產生. 你可以在 "panasonic_devices.json" 找到家電型號資訊,接著把型號資訊以及 "panasonic_commands.json" 提供給我 或是發到 issue. 55 | 56 | 打賞 57 | 58 | | LINE Pay | LINE Bank | JKao Pay | 59 | | :------------: | :------------: | :------------: | 60 | | Line Pay | Line Bank | JKo Pay | -------------------------------------------------------------------------------- /custom_components/panasonic_ems2/binary_sensor.py: -------------------------------------------------------------------------------- 1 | """ Panasonic Smart Home Binary Sensor""" 2 | import logging 3 | from datetime import timedelta 4 | 5 | from homeassistant.components.binary_sensor import ( 6 | BinarySensorDeviceClass, 7 | BinarySensorEntity 8 | ) 9 | 10 | from .core.base import PanasonicBaseEntity 11 | from .core.const import ( 12 | DOMAIN, 13 | DATA_CLIENT, 14 | DATA_COORDINATOR, 15 | SAA_BINARY_SENSORS, 16 | PanasonicBinarySensorDescription 17 | ) 18 | SCAN_INTERVAL = timedelta(seconds=60) 19 | 20 | _LOGGER = logging.getLogger(__name__) 21 | 22 | 23 | async def async_setup_entry(hass, entry, async_add_entities) -> bool: 24 | client = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] 25 | coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] 26 | devices = coordinator.data 27 | 28 | try: 29 | entities = [] 30 | 31 | for device_gwid, info in devices.items(): 32 | device_type = int(info.get("DeviceType")) 33 | if not client.is_supported(info.get("ModelType", "")): 34 | continue 35 | for dev in info.get("Information", {}): 36 | device_id = dev["DeviceID"] 37 | status = dev["status"] 38 | 39 | for saa, sensors in SAA_BINARY_SENSORS.items(): 40 | if device_type == saa: 41 | for description in sensors: 42 | if description.key in status: 43 | entities.extend( 44 | [PanasonicBinarySensor( 45 | coordinator, device_gwid, device_id, client, info, description)] 46 | ) 47 | 48 | async_add_entities(entities) 49 | except AttributeError as ex: 50 | _LOGGER.error(ex) 51 | 52 | return True 53 | 54 | def get_key_from_dict(dictionary, value): 55 | """ get key from dictionary by value""" 56 | for key, val in dictionary.items(): 57 | if value == val: 58 | return key 59 | return None 60 | 61 | 62 | class PanasonicBinarySensor(PanasonicBaseEntity, BinarySensorEntity): 63 | """Implementation of a Panasonic binary sensor.""" 64 | entity_description: PanasonicBinarySensorDescription 65 | 66 | def __init__( 67 | self, 68 | coordinator, 69 | device_gwid, 70 | device_id, 71 | client, 72 | info, 73 | description 74 | ): 75 | super().__init__(coordinator, device_gwid, device_id, client, info) 76 | self.entity_description = description 77 | 78 | @property 79 | def name(self): 80 | """Return the name of the binary sensor.""" 81 | name = self.client.get_command_name(self.device_gwid, self.entity_description.key) 82 | if name is not None: 83 | return "{} {}".format( 84 | self.info["NickName"], name 85 | ) 86 | return "{} {}".format( 87 | self.info["NickName"], self.entity_description.name 88 | ) 89 | 90 | @property 91 | def unique_id(self): 92 | """Return the unique of the sensor.""" 93 | return "{}_{}_{}".format( 94 | self.device_gwid, 95 | self.device_id, 96 | self.entity_description.key 97 | ) 98 | 99 | @property 100 | def is_on(self) -> bool: 101 | """Return the state of the binary sensor.""" 102 | status = self.get_status(self.coordinator.data) 103 | value = status.get(self.entity_description.key, False) 104 | return value 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) 2 | ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/tsunglung/panasonic_ems2?style=for-the-badge) 3 | [![GitHub license](https://img.shields.io/github/license/tsunglung/panasonic_ems2?style=for-the-badge)](https://github.com/osk2/panasonic_smart_app/blob/master/LICENSE) 4 | 5 | 6 | [繁體中文](README_zh-tw.md) | [English](README.md) 7 | 8 | # Panasonic IoT TW 9 | 10 | Home Assistant integration for Panasonic IoT TW [Android](https://play.google.com/store/apps/details?id=com.panasonic.smart&hl=zh_TW&gl=US&pli=1) [iOS](https://apps.apple.com/tw/app/panasonic-iot-tw/id904484053). 11 | 12 | This integration allows you to control your Panasonic IoT appliances. 13 | 14 | This project is forked from [Osk2's](https://github.com/osk2) [panasonic_smart_app](https://github.com/osk2/panasonic_smart_appp). 15 | Buy Me A Coffee 16 | 17 | ## Note 18 | 19 | 1. This integration only support the latest version of Panasonic IoT module, please use the latest version of IoT module. 20 | 2. The code was refacotred, currently support Climate, Washing Machine, Fridge, Dehumidifier,ERV and Weight Plate. 21 | 3. The Panasonic IoT appliances have a lot of models, so some appliances may support well. Welcome to report the issue to me to fix it. 22 | 23 | # Installation 24 | 25 | You can install component with [HACS](https://hacs.xyz/) custom repo: HACS > Integrations > 3 dots (upper top corner) > Custom repositories > URL: `tsunglung/panasonic_ems2` > Category: Integration 26 | 27 | Then restart Home Assistant. 28 | 29 | ### Manually Installation 30 | 31 | Copy `panasonic_ems2` folder of custom_components in this repository to `custom_components` folder in your config folder. 32 | 33 | # Configuration 34 | 35 | **Please use the config flow of Home Assistant** 36 | 37 | 1. With GUI. Configuration > Integration > Add Integration > `Panasonic Smart IoT` 38 | 1. If the integration didn't show up in the list please REFRESH the page 39 | 2. If the integration is still not in the list, you need to clear the browser cache. 40 | 2. Enter the Login info (email and password of [Panasonic Cloud](https://club.panasonic.tw/)) 41 | 3. Enjoy 42 | 43 | # Help to add your Panasonic IoT appliances in this integration 44 | 45 | If you do not see any device or entity is not normal after add this integration, your appliances may not corretly supported by this integration. 46 | You can help to improve this integration via send the information of your appliances to me to debug. 47 | 48 | **Method** 49 | 50 | 1. Download and install [Python](https://www.python.org/downloads/) 51 | 2. Download the script [panasonic_ems2.py](https://github.com/tsunglung/panasonic_ems2/raw/master/scripts/panasonic_ems2.py) to your PC or MacOS 52 | 3. Find the downloaded script, use CMD of Windows or Terminal of MacOS, "cd [your Download Path]" 53 | 4. Run the following command and login your Panasonic Cloud Account. 54 | ``` 55 | pip install request 56 | python panasonic_ems2.py 57 | ``` 58 | 5. There are two files generated. You can find the model info in "panasonic_devices.json" then send the model info and the "panasonic_commands.json" to me or crate a issue. 59 | 60 | Buy Me A Coffee 61 | 62 | | LINE Pay | LINE Bank | JKao Pay | 63 | | :------------: | :------------: | :------------: | 64 | | Line Pay | Line Bank | JKo Pay | 65 | -------------------------------------------------------------------------------- /custom_components/panasonic_ems2/number.py: -------------------------------------------------------------------------------- 1 | """ Panasonic Smart Home Number""" 2 | import logging 3 | from datetime import timedelta 4 | 5 | from homeassistant.components.number import ( 6 | NumberEntity 7 | ) 8 | 9 | from .core.base import PanasonicBaseEntity 10 | from .core.const import ( 11 | DOMAIN, 12 | DATA_CLIENT, 13 | DATA_COORDINATOR, 14 | SAA_NUMBERS, 15 | PanasonicNumberDescription 16 | ) 17 | SCAN_INTERVAL = timedelta(seconds=60) 18 | 19 | _LOGGER = logging.getLogger(__name__) 20 | 21 | async def async_setup_entry(hass, entry, async_add_entities) -> bool: 22 | client = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] 23 | coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] 24 | devices = coordinator.data 25 | 26 | try: 27 | entities = [] 28 | 29 | for device_gwid, info in devices.items(): 30 | device_type = int(info.get("DeviceType")) 31 | if not client.is_supported(info.get("ModelType", "")): 32 | continue 33 | for dev in info.get("Information", {}): 34 | device_id = dev["DeviceID"] 35 | status = dev["status"] 36 | 37 | for saa, numbers in SAA_NUMBERS.items(): 38 | if device_type == saa: 39 | for description in numbers: 40 | if description.key in status: 41 | entities.extend( 42 | [PanasonicNumber( 43 | coordinator, device_gwid, device_id, client, info, description)] 44 | ) 45 | 46 | async_add_entities(entities) 47 | except AttributeError as ex: 48 | _LOGGER.error(ex) 49 | 50 | return True 51 | 52 | 53 | def get_key_from_dict(dictionary, value): 54 | """ get key from dictionary by value""" 55 | for key, val in dictionary.items(): 56 | if value == val: 57 | return key 58 | return None 59 | 60 | 61 | class PanasonicNumber(PanasonicBaseEntity, NumberEntity): 62 | """Implementation of a Panasonic number.""" 63 | entity_description: PanasonicNumberDescription 64 | 65 | def __init__( 66 | self, 67 | coordinator, 68 | device_gwid, 69 | device_id, 70 | client, 71 | info, 72 | description 73 | ): 74 | super().__init__(coordinator, device_gwid, device_id, client, info) 75 | self.entity_description = description 76 | self._range = client.get_range(device_gwid, self.entity_description.key) 77 | 78 | self._attr_native_min_value = 0 79 | self._attr_native_max_value = 1 80 | 81 | if self._range: 82 | self._attr_native_min_value = list(self._range.values())[0] 83 | self._attr_native_max_value = list(self._range.values())[-1] 84 | else: 85 | self._attr_native_min_value = self.entity_description.native_min_value 86 | self._attr_native_max_value = self.entity_description.native_max_value 87 | 88 | @property 89 | def name(self): 90 | """Return the name of the number.""" 91 | name = self.client.get_command_name(self.device_gwid, self.entity_description.key) 92 | if name is not None: 93 | return "{} {}".format( 94 | self.info["NickName"], name 95 | ) 96 | return "{} {}".format( 97 | self.info["NickName"], self.entity_description.name 98 | ) 99 | 100 | @property 101 | def unique_id(self): 102 | """Return the unique of the number.""" 103 | return "{}_{}_{}".format( 104 | self.device_gwid, 105 | self.device_id, 106 | self.entity_description.key 107 | ) 108 | 109 | @property 110 | def native_value(self) -> float | None: 111 | """Return the value reported by the number.""" 112 | status = self.get_status(self.coordinator.data) 113 | if status: 114 | value = float(status[self.entity_description.key]) 115 | return value 116 | return None 117 | 118 | async def async_set_native_value(self, value: float) -> None: 119 | """Set new value.""" 120 | gwid = self.device_gwid 121 | device_id = self.device_id 122 | 123 | await self.client.set_device( 124 | gwid, device_id, self.entity_description.key, int(value)) 125 | await self.client.update_device(gwid, device_id) 126 | self.async_write_ha_state() 127 | -------------------------------------------------------------------------------- /custom_components/panasonic_ems2/__init__.py: -------------------------------------------------------------------------------- 1 | """ Panasonic Smart Home """ 2 | import logging 3 | import asyncio 4 | from datetime import datetime, timedelta 5 | 6 | from homeassistant.config_entries import ConfigEntry 7 | from homeassistant.const import CONF_USERNAME, CONF_PASSWORD 8 | from homeassistant.core import HomeAssistant 9 | from homeassistant.exceptions import ConfigEntryNotReady 10 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 11 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed 12 | 13 | from .core.cloud import PanasonicSmartHome 14 | from .core.const import ( 15 | CONF_UPDATE_INTERVAL, 16 | DATA_COORDINATOR, 17 | DATA_CLIENT, 18 | DEFAULT_UPDATE_INTERVAL, 19 | DOMAIN, 20 | DOMAINS, 21 | UPDATE_LISTENER 22 | ) 23 | 24 | _LOGGER = logging.getLogger(__name__) 25 | 26 | 27 | async def async_setup(hass: HomeAssistant, hass_config: dict): 28 | """ setup """ 29 | config = hass_config.get(DOMAIN) or {} 30 | 31 | hass.data[DOMAIN] = { 32 | 'config': config, 33 | } 34 | 35 | return True 36 | 37 | 38 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): 39 | """Support Panasonic Smart Home.""" 40 | 41 | # migrate data (also after first setup) to options 42 | if entry.data: 43 | hass.config_entries.async_update_entry( 44 | entry, data={}, options=entry.data) 45 | username = entry.options.get(CONF_USERNAME) 46 | password = entry.options.get(CONF_PASSWORD) 47 | session = async_get_clientsession(hass) 48 | client = PanasonicSmartHome(hass, session, username, password) 49 | 50 | updated_options = entry.options.copy() 51 | await client.async_check_tokens() 52 | 53 | if not client.token: 54 | raise ConfigEntryNotReady 55 | 56 | update_interval = entry.options.get(CONF_UPDATE_INTERVAL, None) 57 | account_number = await client.get_user_accounts_number() 58 | await client.get_user_devices() 59 | select_devices = entry.options.get("select_devices", {}) 60 | await client.set_select_devices(select_devices) 61 | 62 | recommand_interval = DEFAULT_UPDATE_INTERVAL 63 | if len(select_devices) >= 1: 64 | recommand_interval = int(3600 / (150 / (len(select_devices) + 1)) / account_number) 65 | elif isinstance(client.devices_number, int): 66 | recommand_interval = int(3600 / (150 / (client.devices_number + 1)) / account_number) 67 | if update_interval is None: 68 | # The maximal API access is 150 per hour 69 | update_interval = max(recommand_interval, DEFAULT_UPDATE_INTERVAL) 70 | updated_options[CONF_UPDATE_INTERVAL] = update_interval 71 | else: 72 | if (update_interval < recommand_interval or 73 | update_interval - 24 >= recommand_interval 74 | ): 75 | updated_options[CONF_UPDATE_INTERVAL] = recommand_interval 76 | 77 | # await client.get_device_ip() # disabled because no help 78 | hass.config_entries.async_update_entry( 79 | entry=entry, 80 | options=updated_options, 81 | ) 82 | 83 | coordinator = DataUpdateCoordinator( 84 | hass, 85 | _LOGGER, 86 | name=f"Panasonic Smart Home for {username}", 87 | update_method=client.async_update_data, 88 | update_interval=timedelta(seconds=int(update_interval)), 89 | ) 90 | 91 | await coordinator.async_refresh() 92 | 93 | if not coordinator.last_update_success: 94 | raise ConfigEntryNotReady 95 | 96 | hass.data[DOMAIN][entry.entry_id] = { 97 | DATA_CLIENT: client, 98 | DATA_COORDINATOR: coordinator, 99 | } 100 | 101 | # init setup for each supported domains 102 | await hass.config_entries.async_forward_entry_setups(entry, DOMAINS) 103 | 104 | # add update handler 105 | if not entry.update_listeners: 106 | update_listener = entry.add_update_listener(async_update_options) 107 | hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener 108 | 109 | return True 110 | 111 | 112 | async def async_update_options(hass: HomeAssistant, entry: ConfigEntry): 113 | """ Update Optioins if available """ 114 | await hass.config_entries.async_reload(entry.entry_id) 115 | 116 | 117 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): 118 | """ Unload Entry """ 119 | client = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] 120 | # await client.logout() 121 | 122 | unload_ok = all( 123 | await asyncio.gather( 124 | *[ 125 | hass.config_entries.async_forward_entry_unload(entry, domain) 126 | for domain in DOMAINS 127 | ] 128 | ) 129 | ) 130 | if unload_ok: 131 | update_listener = hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] 132 | update_listener() 133 | hass.data[DOMAIN].pop(entry.entry_id) 134 | if not hass.data[DOMAIN]: 135 | hass.data.pop(DOMAIN) 136 | return unload_ok 137 | 138 | -------------------------------------------------------------------------------- /custom_components/panasonic_ems2/select.py: -------------------------------------------------------------------------------- 1 | """ Panasonic Smart Home Select""" 2 | import logging 3 | from datetime import timedelta 4 | 5 | from homeassistant.components.select import ( 6 | SelectEntity 7 | ) 8 | 9 | from .core.base import PanasonicBaseEntity 10 | from .core.const import ( 11 | DOMAIN, 12 | DATA_CLIENT, 13 | DATA_COORDINATOR, 14 | SAA_SELECTS, 15 | DEVICE_TYPE_WASHING_MACHINE, 16 | WASHING_MACHINE_SELECTS, 17 | PanasonicSelectDescription 18 | ) 19 | SCAN_INTERVAL = timedelta(seconds=60) 20 | 21 | _LOGGER = logging.getLogger(__name__) 22 | 23 | 24 | async def async_setup_entry(hass, entry, async_add_entities) -> bool: 25 | client = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] 26 | coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] 27 | devices = coordinator.data 28 | 29 | try: 30 | entities = [] 31 | 32 | for device_gwid, info in devices.items(): 33 | device_type = int(info.get("DeviceType")) 34 | if not client.is_supported(info.get("ModelType", "")): 35 | continue 36 | for dev in info.get("Information", {}): 37 | device_id = dev["DeviceID"] 38 | status = dev["status"] 39 | 40 | for saa, selects in SAA_SELECTS.items(): 41 | if device_type == saa: 42 | for description in selects: 43 | if description.key in status: 44 | entities.extend( 45 | [PanasonicSelect( 46 | coordinator, device_gwid, device_id, client, info, description)] 47 | ) 48 | 49 | if device_type == DEVICE_TYPE_WASHING_MACHINE: 50 | for description in WASHING_MACHINE_SELECTS: 51 | entities.extend( 52 | [PanasonicSelect( 53 | coordinator, device_gwid, 1, client, info, description)] 54 | ) 55 | 56 | async_add_entities(entities) 57 | except AttributeError as ex: 58 | _LOGGER.error(ex) 59 | 60 | return True 61 | 62 | 63 | def get_key_from_dict(dictionary, value): 64 | """ get key from dictionary by value""" 65 | for key, val in dictionary.items(): 66 | if value == val: 67 | return key 68 | return None 69 | 70 | 71 | class PanasonicSelect(PanasonicBaseEntity, SelectEntity): 72 | """Implementation of a Panasonic select.""" 73 | entity_description: PanasonicSelectDescription 74 | 75 | def __init__( 76 | self, 77 | coordinator, 78 | device_gwid, 79 | device_id, 80 | client, 81 | info, 82 | description 83 | ): 84 | super().__init__(coordinator, device_gwid, device_id, client, info) 85 | self.entity_description = description 86 | self._range = {} 87 | 88 | @property 89 | def name(self): 90 | """Return the name of the select.""" 91 | name = self.client.get_command_name(self.device_gwid, self.entity_description.key) 92 | if name is not None: 93 | return "{} {}".format( 94 | self.info["NickName"], name 95 | ) 96 | return "{} {}".format( 97 | self.info["NickName"], self.entity_description.name 98 | ) 99 | 100 | @property 101 | def unique_id(self): 102 | """Return the unique of the select.""" 103 | return "{}_{}_{}".format( 104 | self.device_gwid, 105 | self.device_id, 106 | self.entity_description.key 107 | ) 108 | 109 | @property 110 | def options(self) -> list: 111 | """Return a set of selectable options.""" 112 | rng = self.client.get_range(self.device_gwid, self.entity_description.key) 113 | if len(rng) >= 1: 114 | self._range = rng 115 | return list(rng.keys()) 116 | for idx in range(len(self.entity_description.options)): 117 | option = self.entity_description.options[idx] 118 | self._range[option] = int(self.entity_description.options_value[idx]) 119 | return self.entity_description.options 120 | 121 | @property 122 | def current_option(self) -> str | None: 123 | """Return the selected entity option to represent the entity state.""" 124 | status = self.get_status(self.coordinator.data) 125 | if status: 126 | value = int(status.get(self.entity_description.key, "0")) 127 | return get_key_from_dict(self._range, value) 128 | return None 129 | 130 | async def async_select_option(self, option: str) -> None: 131 | """Change the selected option.""" 132 | value = self._range[option] 133 | gwid = self.device_gwid 134 | device_id = self.device_id 135 | 136 | await self.client.set_device( 137 | gwid, device_id, self.entity_description.key, int(value)) 138 | await self.client.update_device(gwid, device_id) 139 | self.async_write_ha_state() 140 | -------------------------------------------------------------------------------- /custom_components/panasonic_ems2/sensor.py: -------------------------------------------------------------------------------- 1 | """ Panasonic Smart Home Sensor""" 2 | import logging 3 | from datetime import timedelta 4 | 5 | from homeassistant.components.sensor import ( 6 | SensorDeviceClass, 7 | SensorEntity 8 | ) 9 | 10 | from .core.base import PanasonicBaseEntity 11 | from .core.const import ( 12 | DOMAIN, 13 | DATA_CLIENT, 14 | DATA_COORDINATOR, 15 | SAA_SENSORS, 16 | DEVICE_TYPE_FRIDGE, 17 | DEVICE_TYPE_WASHING_MACHINE, 18 | DEVICE_TYPE_WEIGHT_PLATE, 19 | WASHING_MACHINE_SENSORS, 20 | WEIGHT_PLATE_SENSORS, 21 | PanasonicSensorDescription 22 | ) 23 | SCAN_INTERVAL = timedelta(seconds=60) 24 | 25 | _LOGGER = logging.getLogger(__name__) 26 | 27 | 28 | async def async_setup_entry(hass, entry, async_add_entities) -> bool: 29 | client = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] 30 | coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] 31 | devices = coordinator.data 32 | 33 | try: 34 | entities = [] 35 | 36 | for device_gwid, info in devices.items(): 37 | device_type = int(info.get("DeviceType")) 38 | if not client.is_supported(info.get("ModelType", "")): 39 | continue 40 | for dev in info.get("Information", {}): 41 | device_id = dev["DeviceID"] 42 | status = dev["status"] 43 | 44 | for saa, sensors in SAA_SENSORS.items(): 45 | if device_type == saa: 46 | for description in sensors: 47 | if description.key in status: 48 | entities.extend( 49 | [PanasonicSensor( 50 | coordinator, device_gwid, device_id, client, info, description)] 51 | ) 52 | 53 | if device_type == DEVICE_TYPE_WASHING_MACHINE: 54 | for description in WASHING_MACHINE_SENSORS: 55 | entities.extend( 56 | [PanasonicSensor( 57 | coordinator, device_gwid, 1, client, info, description)] 58 | ) 59 | 60 | if device_type == DEVICE_TYPE_WEIGHT_PLATE: 61 | for description in WEIGHT_PLATE_SENSORS: 62 | entities.extend( 63 | [PanasonicSensor( 64 | coordinator, device_gwid, 1, client, info, description)] 65 | ) 66 | 67 | async_add_entities(entities) 68 | except AttributeError as ex: 69 | _LOGGER.error(ex) 70 | 71 | return True 72 | 73 | def get_key_from_dict(dictionary, value): 74 | """ get key from dictionary by value""" 75 | for key, val in dictionary.items(): 76 | if value == val: 77 | return key 78 | return None 79 | 80 | 81 | class PanasonicSensor(PanasonicBaseEntity, SensorEntity): 82 | """Implementation of a Panasonic sensor.""" 83 | entity_description: PanasonicSensorDescription 84 | 85 | def __init__( 86 | self, 87 | coordinator, 88 | device_gwid, 89 | device_id, 90 | client, 91 | info, 92 | description 93 | ): 94 | super().__init__(coordinator, device_gwid, device_id, client, info) 95 | self.entity_description = description 96 | 97 | @property 98 | def name(self): 99 | """Return the name of the sensor.""" 100 | name = self.client.get_command_name(self.device_gwid, self.entity_description.key) 101 | if name is not None: 102 | return "{} {}".format( 103 | self.info["NickName"], name 104 | ) 105 | return "{} {}".format( 106 | self.info["NickName"], self.entity_description.name 107 | ) 108 | 109 | @property 110 | def unique_id(self): 111 | """Return the unique of the sensor.""" 112 | return "{}_{}_{}".format( 113 | self.device_gwid, 114 | self.device_id, 115 | self.entity_description.key 116 | ) 117 | 118 | @property 119 | def native_value(self): 120 | """Return the state of the sensor.""" 121 | status = self.get_status(self.coordinator.data) 122 | if self.entity_description.device_class == SensorDeviceClass.ENUM: 123 | rng = self.client.get_range(self.device_gwid, self.entity_description.key) 124 | value = status.get(self.entity_description.key, 0) 125 | if len(rng) >= 1: 126 | return get_key_from_dict(rng, int(value)) 127 | return value 128 | value = status.get(self.entity_description.key, None) 129 | device_type = int(self.info.get("DeviceType")) 130 | if device_type != DEVICE_TYPE_FRIDGE: 131 | if self.entity_description.device_class == SensorDeviceClass.TEMPERATURE: 132 | if value < -1 or value > 50: 133 | return None 134 | if self.entity_description.device_class == SensorDeviceClass.HUMIDITY: 135 | if value < 30: 136 | return None 137 | if self.entity_description.device_class == SensorDeviceClass.ENERGY: 138 | if value is not None: 139 | if isinstance(value, str): 140 | value = float(value.replace("-", "")) 141 | value = float(value * 0.1) 142 | if value < 1: 143 | return None 144 | return value 145 | 146 | # async def async_update(self): 147 | # """Fetch state from the device.""" 148 | # await self.coordinator.async_request_refresh() -------------------------------------------------------------------------------- /custom_components/panasonic_ems2/switch.py: -------------------------------------------------------------------------------- 1 | """ Panasonic Smart Home Switch""" 2 | import logging 3 | import asyncio 4 | from datetime import timedelta 5 | 6 | from homeassistant.components.switch import ( 7 | SwitchEntity 8 | ) 9 | from homeassistant.const import STATE_UNAVAILABLE 10 | 11 | from .core.base import PanasonicBaseEntity 12 | from .core.const import ( 13 | DOMAIN, 14 | DATA_CLIENT, 15 | DATA_COORDINATOR, 16 | DEVICE_TYPE_LIGHT, 17 | DEVICE_TYPE_WASHING_MACHINE, 18 | LIGHT_POWER, 19 | LIGHT_OPERATION_STATE, 20 | WASHING_MACHINE_SWITCHES, 21 | SAA_SWITCHES, 22 | PanasonicSwitchDescription 23 | ) 24 | SCAN_INTERVAL = timedelta(seconds=60) 25 | 26 | _LOGGER = logging.getLogger(__name__) 27 | 28 | 29 | async def async_setup_entry(hass, entry, async_add_entities) -> bool: 30 | client = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] 31 | coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] 32 | devices = coordinator.data 33 | 34 | try: 35 | entities = [] 36 | 37 | for device_gwid, info in devices.items(): 38 | device_type = int(info.get("DeviceType")) 39 | if not client.is_supported(info.get("ModelType", "")): 40 | continue 41 | for dev in info.get("Information", {}): 42 | device_id = dev["DeviceID"] 43 | status = dev["status"] 44 | 45 | for saa, switchs in SAA_SWITCHES.items(): 46 | if device_type == saa: 47 | for description in switchs: 48 | if description.key in status: 49 | entities.extend( 50 | [PanasonicSwitch( 51 | coordinator, device_gwid, device_id, client, info, description)] 52 | ) 53 | 54 | if device_type == DEVICE_TYPE_WASHING_MACHINE: 55 | for description in WASHING_MACHINE_SWITCHES: 56 | if True: 57 | entities.extend( 58 | [PanasonicSwitch( 59 | coordinator, device_gwid, 1, client, info, description)] 60 | ) 61 | 62 | async_add_entities(entities) 63 | except AttributeError as ex: 64 | _LOGGER.error(ex) 65 | 66 | return True 67 | 68 | 69 | class PanasonicSwitch(PanasonicBaseEntity, SwitchEntity): 70 | """Implementation of a Panasonic switch.""" 71 | entity_description: PanasonicSwitchDescription 72 | 73 | def __init__( 74 | self, 75 | coordinator, 76 | device_gwid, 77 | device_id, 78 | client, 79 | info, 80 | description 81 | ): 82 | super().__init__(coordinator, device_gwid, device_id, client, info) 83 | self.entity_description = description 84 | 85 | @property 86 | def name(self): 87 | """Return the name of the switch.""" 88 | name = self.client.get_command_name(self.device_gwid, self.entity_description.key) 89 | 90 | if name is not None: 91 | # hard code 92 | if "nanoe" in name: 93 | return "{} {}".format( 94 | self.info["NickName"], self.entity_description.name 95 | ) 96 | device_name = "" 97 | for dev in self.info.get("Devices", {}): 98 | if self.device_id == dev.get("DeviceID", 0): 99 | device_name = dev.get("Name", "") 100 | break 101 | 102 | return "{} {}{}".format( 103 | self.info["NickName"], device_name, name 104 | ) 105 | return "{} {}".format( 106 | self.info["NickName"], self.entity_description.name 107 | ) 108 | 109 | @property 110 | def unique_id(self): 111 | """Return the unique of the switch.""" 112 | return "{}_{}_{}".format( 113 | self.device_gwid, 114 | self.device_id, 115 | self.entity_description.key 116 | ) 117 | 118 | @property 119 | def is_on(self) -> int: 120 | device_id = self.device_id 121 | info = self.coordinator.data 122 | status = self.get_status(info) 123 | 124 | avaiable = status.get(self.entity_description.key, None) 125 | if avaiable is None: 126 | return STATE_UNAVAILABLE 127 | 128 | if ((int(info[self.device_gwid].get("DeviceType")) == DEVICE_TYPE_LIGHT) and 129 | (self.entity_description.key == LIGHT_POWER)): 130 | for device in info[self.device_gwid]["Information"]: 131 | operation_state = device["status"].get(LIGHT_OPERATION_STATE, None) 132 | if operation_state != None: 133 | state = (int(operation_state) & (1 << (device_id - 1))) >> (device_id - 1) 134 | return bool(state) 135 | return STATE_UNAVAILABLE 136 | 137 | state = status.get(self.entity_description.key) 138 | if not isinstance(state, int): 139 | return STATE_UNAVAILABLE 140 | return bool(int(status.get(self.entity_description.key, 0))) 141 | 142 | async def async_turn_on(self) -> None: 143 | gwid = self.device_gwid 144 | device_id = self.device_id 145 | await self.client.set_device(gwid, device_id, self.entity_description.key, 1) 146 | await asyncio.sleep(1) 147 | await self.client.update_device(gwid, device_id) 148 | self.async_write_ha_state() 149 | 150 | async def async_turn_off(self) -> None: 151 | gwid = self.device_gwid 152 | device_id = self.device_id 153 | await self.client.set_device(gwid, device_id, self.entity_description.key, 0) 154 | await asyncio.sleep(1) 155 | await self.client.update_device(gwid, device_id) 156 | self.async_write_ha_state() 157 | -------------------------------------------------------------------------------- /custom_components/panasonic_ems2/humidifier.py: -------------------------------------------------------------------------------- 1 | """ Panasonic Smart Home Fan""" 2 | import logging 3 | import asyncio 4 | 5 | from homeassistant.components.humidifier import ( 6 | HumidifierDeviceClass, 7 | HumidifierEntityFeature, 8 | HumidifierEntity 9 | ) 10 | 11 | from .core.base import PanasonicBaseEntity 12 | from .core.const import ( 13 | DOMAIN, 14 | DATA_CLIENT, 15 | DATA_COORDINATOR, 16 | DEVICE_TYPE_DEHUMIDIFIER, 17 | DEHUMIDIFIER_DEFAULT_MODES, 18 | DEHUMIDIFIER_POWER, 19 | DEHUMIDIFIER_MODE, 20 | DEHUMIDIFIER_TARGET_HUMIDITY, 21 | DEHUMIDIFIER_MAX_HUMIDITY, 22 | DEHUMIDIFIER_MIN_HUMIDITY 23 | ) 24 | 25 | _LOGGER = logging.getLogger(__name__) 26 | 27 | 28 | def get_key_from_dict(dictionary, value): 29 | """ get key from dictionary by value""" 30 | for key, val in dictionary.items(): 31 | if value == val: 32 | return key 33 | return None 34 | 35 | 36 | async def async_setup_entry(hass, entry, async_add_entities) -> bool: 37 | client = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] 38 | coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] 39 | devices = coordinator.data 40 | humidifer = [] 41 | 42 | for device_gwid, info in devices.items(): 43 | device_type = int(info.get("DeviceType")) 44 | if not client.is_supported(info.get("ModelType", "")): 45 | continue 46 | for dev in info.get("Information", {}): 47 | device_id = dev["DeviceID"] 48 | if device_type == DEVICE_TYPE_DEHUMIDIFIER: 49 | humidifer.append( 50 | PanasonicHumidifier( 51 | coordinator, 52 | device_gwid, 53 | device_id, 54 | client, 55 | info, 56 | ) 57 | ) 58 | 59 | async_add_entities(humidifer, True) 60 | 61 | return True 62 | 63 | def get_key_from_dict(dictionary, value): 64 | """ get key from dictionary by value""" 65 | for key, val in dictionary.items(): 66 | if value == val: 67 | return key 68 | return None 69 | 70 | 71 | class PanasonicHumidifier(PanasonicBaseEntity, HumidifierEntity): 72 | 73 | _attr_supported_features = HumidifierEntityFeature.MODES 74 | _attr_device_class = HumidifierDeviceClass.HUMIDIFIER 75 | 76 | def __init__( 77 | self, 78 | coordinator, 79 | device_gwid, 80 | device_id, 81 | client, 82 | info, 83 | ): 84 | super().__init__(coordinator, device_gwid, device_id, client, info) 85 | device_type = info.get("DeviceType", None) 86 | self._device_type = device_type 87 | self._modes = {} 88 | self._state = None 89 | 90 | rng = client.get_range(device_gwid, DEHUMIDIFIER_TARGET_HUMIDITY) 91 | 92 | try: 93 | self._attr_min_humidity = int(list(rng.keys())[0].replace("%", "")) 94 | except: 95 | self._attr_min_humidity = DEHUMIDIFIER_MIN_HUMIDITY 96 | try: 97 | self._attr_max_humidity = int(list(rng.keys())[-1].replace("%", "")) 98 | except: 99 | self._attr_max_humidity = DEHUMIDIFIER_MAX_HUMIDITY 100 | 101 | @property 102 | def available_modes(self) -> list: 103 | """Return a list of available modes. 104 | 105 | Requires HumidifierEntityFeature.MODES. 106 | """ 107 | rng = self.client.get_range(self.device_gwid, DEHUMIDIFIER_MODE) 108 | if len(rng) >= 1: 109 | self._modes = rng 110 | return list(rng.keys()) 111 | self._modes = DEHUMIDIFIER_DEFAULT_MODES 112 | return list(DEHUMIDIFIER_DEFAULT_MODES.keys()) 113 | 114 | @property 115 | def mode(self) -> str | None: 116 | """Return the current mode, e.g., home, auto, baby. 117 | 118 | Requires HumidifierEntityFeature.MODES. 119 | """ 120 | status = self.get_status(self.coordinator.data) 121 | if status: 122 | value = int(status[DEHUMIDIFIER_MODE]) 123 | return get_key_from_dict(self._modes, value) 124 | return None 125 | 126 | @property 127 | def is_on(self): 128 | """Return true if device is on.""" 129 | status = self.get_status(self.coordinator.data) 130 | # _LOGGER.error(f"is on {status}") 131 | self._state = bool(int(status.get(DEHUMIDIFIER_POWER, 0))) 132 | 133 | return self._state 134 | 135 | @property 136 | def target_humidity(self) -> int: 137 | status = self.get_status(self.coordinator.data) 138 | if status: 139 | try: 140 | rng = self.client.get_range(self.device_gwid, DEHUMIDIFIER_TARGET_HUMIDITY) 141 | if len(rng) >= 1: 142 | self._range = rng 143 | value = int(status[DEHUMIDIFIER_TARGET_HUMIDITY]) 144 | return int(get_key_from_dict(rng, value).replace("%", "")) 145 | except: 146 | return None 147 | return None 148 | 149 | async def async_set_humidity(self, humidity: int) -> None: 150 | """Set new target humidity.""" 151 | gwid = self.device_gwid 152 | device_id = self.device_id 153 | 154 | await self.client.set_device(gwid, device_id, DEHUMIDIFIER_TARGET_HUMIDITY, humidity) 155 | await self.client.update_device(gwid, device_id) 156 | self.async_write_ha_state() 157 | 158 | async def async_set_mode(self, mode: str) -> None: 159 | """Set new mode.""" 160 | gwid = self.device_gwid 161 | device_id = self.device_id 162 | 163 | value = self._modes[mode] 164 | 165 | await self.client.set_device(gwid, device_id, DEHUMIDIFIER_MODE, value) 166 | await self.client.update_device(gwid, device_id) 167 | self.async_write_ha_state() 168 | 169 | async def async_turn_on(self) -> None: 170 | """Turn the device on.""" 171 | gwid = self.device_gwid 172 | device_id = self.device_id 173 | 174 | await self.client.set_device(gwid, device_id, DEHUMIDIFIER_POWER, 1) 175 | await asyncio.sleep(1) 176 | await self.client.update_device(gwid, device_id) 177 | self.async_write_ha_state() 178 | 179 | async def async_turn_off(self) -> None: 180 | """Turn the device off.""" 181 | gwid = self.device_gwid 182 | device_id = self.device_id 183 | 184 | await self.client.set_device(gwid, device_id, DEHUMIDIFIER_POWER, 0) 185 | await asyncio.sleep(1) 186 | await self.client.update_device(gwid, device_id) 187 | self.async_write_ha_state() 188 | -------------------------------------------------------------------------------- /custom_components/panasonic_ems2/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow to configure Panasonic Samrt Home component.""" 2 | from collections import OrderedDict 3 | from typing import Optional, Any 4 | import asyncio 5 | import voluptuous as vol 6 | 7 | from homeassistant.config_entries import ( 8 | CONN_CLASS_CLOUD_POLL, 9 | ConfigEntry, 10 | ConfigFlow, 11 | OptionsFlow 12 | ) 13 | from homeassistant.const import CONF_USERNAME, CONF_NAME, CONF_PASSWORD 14 | from homeassistant.core import callback 15 | from homeassistant.data_entry_flow import FlowResult 16 | from homeassistant.exceptions import ConfigEntryNotReady 17 | import homeassistant.helpers.config_validation as cv 18 | from homeassistant.helpers.typing import ConfigType 19 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 20 | 21 | from .core.cloud import PanasonicSmartHome 22 | from .core.exceptions import Ems2ExceedRateLimit, Ems2LoginFailed 23 | from .core.const import ( 24 | CONF_UPDATE_INTERVAL, 25 | DEFAULT_UPDATE_INTERVAL, 26 | DOMAIN 27 | ) 28 | 29 | 30 | class PanasonicSmartHomeFlowHandler(ConfigFlow, domain=DOMAIN): 31 | """Handle a Panasonic Samrt Home config flow.""" 32 | 33 | VERSION = 1 34 | CONNECTION_CLASS = CONN_CLASS_CLOUD_POLL 35 | 36 | def __init__(self): 37 | """Initialize flow.""" 38 | self._username: Optional[str] = None 39 | self._password: Optional[str] = None 40 | self._errors: Optional[dict] = {} 41 | self.login_info: Optional[dict] = {} 42 | self.cloud_devices: dict[str, dict[str, Any]] = {} 43 | 44 | @staticmethod 45 | @callback 46 | def async_get_options_flow(config_entry: ConfigEntry): 47 | """ get option flow """ 48 | return OptionsFlowHandler(config_entry) 49 | 50 | async def async_step_user( 51 | self, 52 | user_input: Optional[ConfigType] = None 53 | ) -> FlowResult: 54 | """Handle a flow initialized by the user.""" 55 | if self._async_current_entries(): 56 | return self.async_abort(reason="single_instance_allowed") 57 | 58 | if user_input is not None: 59 | self._set_user_input(user_input) 60 | session = async_get_clientsession(self.hass) 61 | 62 | client = PanasonicSmartHome( 63 | self.hass, 64 | session=session, 65 | account=self._username, 66 | password=self._password 67 | ) 68 | 69 | try: 70 | # force login here 71 | self.login_info = await client.async_check_tokens() 72 | self._name = self._username 73 | if not client.token: 74 | raise ConfigEntryNotReady 75 | 76 | await self.async_set_unique_id(self._username) 77 | 78 | await asyncio.sleep(1) # add sleep 1 to avoid frequency request 79 | devices_raw = await client.get_user_devices() 80 | if len(devices_raw) < 1: 81 | self._errors["status"] = "error" 82 | self._errors["base"] = "network_busy" 83 | raise ConfigEntryNotReady 84 | else: 85 | self.login_info[CONF_USERNAME] = self._username 86 | self.login_info[CONF_PASSWORD] = self._password 87 | if len(devices_raw[0]) == 1: 88 | return self._async_get_entry(self.login_info) 89 | self.cloud_devices = {} 90 | for device in devices_raw: 91 | name = device["NickName"] 92 | model = device["Model"] 93 | list_name = f"{name} - {model}" 94 | self.cloud_devices[list_name] = device 95 | return await self.async_step_select() 96 | 97 | except Ems2ExceedRateLimit: 98 | self._errors["base"] = "rate_limit" 99 | except Ems2LoginFailed: 100 | self._errors["base"] = "auth" 101 | except Exception as e: 102 | self._errors["status"] = "error" 103 | self._errors["base"] = "connection_error" 104 | 105 | fields = OrderedDict() 106 | fields[vol.Required(CONF_USERNAME, 107 | default=self._username or vol.UNDEFINED)] = str 108 | fields[vol.Required(CONF_PASSWORD, 109 | default=self._password or vol.UNDEFINED)] = str 110 | 111 | return self.async_show_form( 112 | step_id="user", 113 | data_schema=vol.Schema(fields), 114 | errors=self._errors 115 | ) 116 | 117 | def extract_cloud_info(self, devices: list[str]) -> None: 118 | """Extract the cloud info.""" 119 | select_devices = {} 120 | for name in devices: 121 | select_devices[name] = self.cloud_devices[name]["GWID"] 122 | self.login_info["select_devices"] = select_devices 123 | 124 | async def async_step_select( 125 | self, user_input: dict[str, Any] | None = None 126 | ) -> FlowResult: 127 | """Handle multiple cloud devices found.""" 128 | errors: dict[str, str] = {} 129 | if user_input is not None: 130 | self.extract_cloud_info(user_input["select_devices"]) 131 | return self._async_get_entry(self.login_info) 132 | 133 | select_schema = vol.Schema( 134 | {vol.Required("select_devices"): cv.multi_select(list(self.cloud_devices))} 135 | ) 136 | 137 | return self.async_show_form( 138 | step_id="select", data_schema=select_schema, errors=errors 139 | ) 140 | 141 | @property 142 | def _name(self): 143 | # pylint: disable=no-member 144 | # https://github.com/PyCQA/pylint/issues/3167 145 | return self.context.get(CONF_NAME) 146 | 147 | @_name.setter 148 | def _name(self, value): 149 | # pylint: disable=no-member 150 | # https://github.com/PyCQA/pylint/issues/3167 151 | self.context[CONF_NAME] = value 152 | self.context["title_placeholders"] = {"name": self._name} 153 | 154 | def _set_user_input(self, user_input): 155 | if user_input is None: 156 | return 157 | self._username = user_input.get(CONF_USERNAME, "") 158 | self._password = user_input.get(CONF_PASSWORD, "") 159 | 160 | @callback 161 | def _async_get_entry(self, login_info: dict): 162 | return self.async_create_entry( 163 | title=self._name, 164 | data=login_info, 165 | ) 166 | 167 | 168 | class OptionsFlowHandler(OptionsFlow): 169 | # pylint: disable=too-few-public-methods 170 | """Handle options flow changes.""" 171 | _username = None 172 | _password = None 173 | _update_interval = 600 174 | 175 | def __init__(self, config_entry): 176 | """Initialize options flow.""" 177 | self.config_entry = config_entry 178 | 179 | async def async_step_init( 180 | self, user_input=None 181 | ) -> FlowResult: 182 | """Manage options.""" 183 | if user_input is not None: 184 | if len(user_input.get(CONF_USERNAME, "")) >= 1: 185 | self._username = user_input.get(CONF_USERNAME) 186 | if len(user_input.get(CONF_PASSWORD, "")) >= 1: 187 | self._password = user_input.get(CONF_PASSWORD) 188 | 189 | self._password = user_input.get( 190 | CONF_UPDATE_INTERVAL, DEFAULT_UPDATE_INTERVAL) 191 | 192 | return self.async_create_entry( 193 | title=self._username, 194 | data={ 195 | CONF_USERNAME: self._username, 196 | CONF_PASSWORD: self._password, 197 | CONF_UPDATE_INTERVAL: self._update_interval, 198 | }, 199 | ) 200 | self._username = self.config_entry.options[CONF_USERNAME] 201 | self._password = self.config_entry.options.get(CONF_PASSWORD, '') 202 | self._update_interval = self.config_entry.options.get( 203 | CONF_UPDATE_INTERVAL, DEFAULT_UPDATE_INTERVAL) 204 | 205 | return self.async_show_form( 206 | step_id="init", 207 | data_schema=vol.Schema( 208 | { 209 | vol.Optional(CONF_UPDATE_INTERVAL, default=self.config_entry.options.get( 210 | CONF_UPDATE_INTERVAL, DEFAULT_UPDATE_INTERVAL 211 | )): vol.All( 212 | vol.Coerce(int), vol.Range( 213 | min=DEFAULT_UPDATE_INTERVAL, max=600) 214 | ) 215 | } 216 | ), 217 | ) 218 | -------------------------------------------------------------------------------- /scripts/panasonic_ems2.py: -------------------------------------------------------------------------------- 1 | """ Panasonic Smart Home """ 2 | import logging 3 | import asyncio 4 | from http import HTTPStatus 5 | import requests 6 | from getpass import getpass 7 | import ast 8 | import json 9 | from datetime import datetime 10 | from typing import Literal, Final 11 | 12 | 13 | HA_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1" 14 | BASE_URL = 'https://ems2.panasonic.com.tw/api' 15 | APP_TOKEN = "D8CBFF4C-2824-4342-B22D-189166FEF503" 16 | 17 | CONTENT_TYPE_JSON: Final = "application/json" 18 | REQUEST_TIMEOUT = 15 19 | 20 | _LOGGER = logging.getLogger(__name__) 21 | 22 | 23 | class Ems2BaseException(Exception): 24 | """ Base exception """ 25 | 26 | 27 | class Ems2TokenNotFound(Ems2BaseException): 28 | """ Refresh token not found """ 29 | 30 | 31 | class Ems2TokenExpired(Ems2BaseException): 32 | """ Token expired """ 33 | 34 | 35 | class Ems2InvalidRefreshToken(Ems2BaseException): 36 | """ Refresh token expired """ 37 | 38 | 39 | class Ems2TooManyRequest(Ems2BaseException): 40 | """ Too many request """ 41 | 42 | 43 | class Ems2LoginFailed(Ems2BaseException): 44 | """ Any other login exception """ 45 | 46 | 47 | class Ems2Expectation(Ems2BaseException): 48 | """ Any other exception """ 49 | 50 | 51 | class Ems2ExceedRateLimit(Ems2BaseException): 52 | """ API reaches rate limit """ 53 | 54 | class apis(object): 55 | 56 | def open_session(): 57 | url = f"{BASE_URL}/userlogin1" 58 | return url 59 | 60 | def get_user_devices(): 61 | url = f"{BASE_URL}/UserGetRegisteredGwList2" 62 | return url 63 | 64 | class PanasonicSmartHome(object): 65 | """ 66 | Panasonic Smart Home Object 67 | """ 68 | def __init__(self, hass, session, account, password): 69 | self.hass = hass 70 | self.email = account 71 | self.password = password 72 | self._session = session 73 | self._devices = [] 74 | self._commands = [] 75 | self._devices_info = {} 76 | self._commands_info = {} 77 | self._cp_token = "" 78 | self._refresh_token = None 79 | self._expires_in = 0 80 | self._expire_time = None 81 | self._token_timeout = None 82 | self._refresh_token_timeout = None 83 | self._mversion = None 84 | self._update_timestamp = None 85 | self._api_counts = 0 86 | self._api_counts_per_hour = 0 87 | 88 | async def request( 89 | self, 90 | method: Literal["GET", "POST"], 91 | headers, 92 | endpoint: str, 93 | params=None, 94 | data=None, 95 | ): 96 | """Shared request method""" 97 | res = {} 98 | headers["user-agent"] = HA_USER_AGENT 99 | headers["Content-Type"] = CONTENT_TYPE_JSON 100 | 101 | self._api_counts = self._api_counts + 1 102 | self._api_counts_per_hour = self._api_counts_per_hour + 1 103 | try: 104 | if self._session: 105 | response = await self._session.request( 106 | method, 107 | url=endpoint, 108 | json=data, 109 | params=params, 110 | headers=headers, 111 | timeout=REQUEST_TIMEOUT 112 | ) 113 | else: 114 | response = requests.request( 115 | method, 116 | endpoint, 117 | params=params, 118 | json=data, 119 | headers=headers 120 | ) 121 | except requests.exceptions.RequestException: 122 | _LOGGER.error(f"Failed fetching data for {self.email}") 123 | return {} 124 | except Exception as e: 125 | # request timeout 126 | _LOGGER.error(f" request exception {e}") 127 | return {} 128 | 129 | if self._session: 130 | if response.status == HTTPStatus.OK: 131 | try: 132 | res = await response.json() 133 | except: 134 | res = {} 135 | elif response.status == HTTPStatus.BAD_REQUEST: 136 | raise Ems2ExceedRateLimit 137 | elif response.status == HTTPStatus.FORBIDDEN: 138 | raise Ems2LoginFailed 139 | elif response.status == HTTPStatus.TOO_MANY_REQUESTS: 140 | raise Ems2TooManyRequest 141 | elif response.status == HTTPStatus.EXPECTATION_FAILED: 142 | raise Ems2Expectation 143 | elif response.status == HTTPStatus.NOT_FOUND: 144 | _LOGGER.warning(f"Use wrong method or parameters") 145 | res = {} 146 | else: 147 | raise Ems2TokenNotFound 148 | else: 149 | if response.status_code == HTTPStatus.OK: 150 | try: 151 | res = ast.literal_eval(response.text) 152 | except Exception as e: 153 | res = {} 154 | elif response.status_code == HTTPStatus.FORBIDDEN: 155 | print("Login failed, Please check your email or password!") 156 | elif response.status_code == HTTPStatus.TOO_MANY_REQUESTS: 157 | print("Login too may times, please wait for one hour, then try again") 158 | elif response.status_code == HTTPStatus.EXPECTATION_FAILED: 159 | print("Exception, please wait for one hour, then try again") 160 | 161 | if isinstance(res, list): 162 | return {"data": res} 163 | 164 | return res 165 | 166 | async def login(self): 167 | """ 168 | Login to get access token. 169 | """ 170 | data = {"MemId": self.email, "PW": self.password, "AppToken": APP_TOKEN} 171 | response = await self.request( 172 | method="POST", headers={}, endpoint=apis.open_session(), data=data 173 | ) 174 | self._cp_token = response.get("CPToken", "") 175 | self._refresh_token = response.get("RefreshToken", "") 176 | self._token_timeout = response.get("TokenTimeOut", "") 177 | self._refresh_token_timeout = response.get("RefreshTokenTimeOut", "") 178 | self._mversion = response.get("MVersion", "") 179 | 180 | async def get_user_devices(self): 181 | """ 182 | List devices that the user has permission 183 | """ 184 | header = {"CPToken": self._cp_token} 185 | response = await self.request( 186 | method="GET", headers=header, endpoint=apis.get_user_devices() 187 | ) 188 | 189 | if isinstance(response, dict): 190 | self._devices = response.get("GwList", []) 191 | self._commands = response.get("CommandList", []) 192 | return self._devices, self._commands 193 | 194 | async def get_devices_info(self): 195 | """ 196 | get devices 197 | """ 198 | await self.login() 199 | info = { 200 | "GwList": [], 201 | "CommandList": [] 202 | } 203 | if self._cp_token: 204 | devices, commands = await self.get_user_devices() 205 | info["GwList"] = devices 206 | info["CommandList"] = commands 207 | else: 208 | print("Have problem to login, please check your account and password!") 209 | return info 210 | 211 | async def get_devices(username, password): 212 | client = PanasonicSmartHome(None, None, username, password) 213 | 214 | info = await client.get_devices_info() 215 | if len(info["GwList"]) >= 1: 216 | with open("panasonic_devices.json", "w", encoding="utf-8") as f_out: 217 | f_out.write(json.dumps(info["GwList"], indent=2, ensure_ascii=False)) 218 | with open("panasonic_commands.json", "w", encoding="utf-8") as f_out: 219 | f_out.write(json.dumps(info["CommandList"], indent=2, ensure_ascii=False)) 220 | print("\nThe panasonic_devices.json and panasonic_commands.json are generated, please send them to the developer!") 221 | 222 | ################################################## 223 | 224 | 225 | def main(): # noqa MC0001 226 | basic_version = "0.0.1" 227 | print(f"Version: {basic_version}\n") 228 | username = input("Account: ") 229 | password = getpass() 230 | asyncio.run(get_devices(username, password)) 231 | 232 | 233 | if __name__ == '__main__': 234 | main() 235 | -------------------------------------------------------------------------------- /custom_components/panasonic_ems2/fan.py: -------------------------------------------------------------------------------- 1 | """ Panasonic Smart Home Fan""" 2 | import logging 3 | import asyncio 4 | 5 | from homeassistant.components.fan import ( 6 | FanEntityFeature, 7 | FanEntity 8 | ) 9 | 10 | from .core.base import PanasonicBaseEntity 11 | from .core.const import ( 12 | DOMAIN, 13 | DATA_CLIENT, 14 | DATA_COORDINATOR, 15 | DEVICE_TYPE_AIRPURIFIER, 16 | DEVICE_TYPE_FAN, 17 | DEVICE_TYPE_WASHING_MACHINE, 18 | FAN_OPERATING_MODE, 19 | FAN_OSCILLATE, 20 | FAN_POWER, 21 | FAN_PRESET_MODES, 22 | FAN_SPEED, 23 | AIRPURIFIER_OPERATING_MODE, 24 | AIRPURIFIER_NANOEX, 25 | AIRPURIFIER_NANOEX_PRESET, 26 | AIRPURIFIER_PRESET_MODES 27 | ) 28 | 29 | _LOGGER = logging.getLogger(__name__) 30 | 31 | 32 | PANASONIC_FAN_TYPE = [ 33 | DEVICE_TYPE_AIRPURIFIER, 34 | DEVICE_TYPE_FAN, 35 | # DEVICE_TYPE_WASHING_MACHINE 36 | ] 37 | 38 | def get_key_from_dict(dictionary, value): 39 | """ get key from dictionary by value""" 40 | for key, val in dictionary.items(): 41 | if value == val: 42 | return key 43 | return None 44 | 45 | 46 | async def async_setup_entry(hass, entry, async_add_entities) -> bool: 47 | client = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] 48 | coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] 49 | devices = coordinator.data 50 | fan = [] 51 | 52 | for device_gwid, info in devices.items(): 53 | device_type = int(info.get("DeviceType")) 54 | if not client.is_supported(info.get("ModelType", "")): 55 | continue 56 | for dev in info.get("Information", {}): 57 | device_id = dev["DeviceID"] 58 | if device_type in PANASONIC_FAN_TYPE: 59 | fan.append( 60 | PanasonicFan( 61 | coordinator, 62 | device_gwid, 63 | device_id, 64 | client, 65 | info, 66 | ) 67 | ) 68 | 69 | async_add_entities(fan, True) 70 | 71 | return True 72 | 73 | 74 | class PanasonicFan(PanasonicBaseEntity, FanEntity): 75 | 76 | def __init__( 77 | self, 78 | coordinator, 79 | device_gwid, 80 | device_id, 81 | client, 82 | info, 83 | ): 84 | super().__init__(coordinator, device_gwid, device_id, client, info) 85 | device_type = int(info.get("DeviceType", 0)) 86 | self._attr_speed_count = 100 87 | if device_type == DEVICE_TYPE_FAN: 88 | self._attr_speed_count = 15 89 | if device_type == DEVICE_TYPE_AIRPURIFIER: 90 | self._attr_speed_count = 6 91 | self._device_type = device_type 92 | self._state = None 93 | 94 | async def async_added_to_hass(self) -> None: 95 | """When entity is added to hass.""" 96 | await super().async_added_to_hass() 97 | 98 | @property 99 | def supported_features(self) -> int: 100 | """Return the list of supported features.""" 101 | feature = FanEntityFeature.SET_SPEED | FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON 102 | status = self.get_status(self.coordinator.data) 103 | 104 | if self._device_type == DEVICE_TYPE_AIRPURIFIER: 105 | if status.get(AIRPURIFIER_NANOEX, None) is not None: 106 | feature |= FanEntityFeature.PRESET_MODE 107 | 108 | if self._device_type == DEVICE_TYPE_FAN: 109 | if status.get(FAN_OPERATING_MODE, None) is not None: 110 | feature |= FanEntityFeature.PRESET_MODE 111 | 112 | if status.get(FAN_OSCILLATE, None) is not None: 113 | feature |= FanEntityFeature.OSCILLATE 114 | 115 | if self._device_type == DEVICE_TYPE_WASHING_MACHINE: 116 | feature |= FanEntityFeature.PRESET_MODE 117 | 118 | return feature 119 | 120 | @property 121 | def is_on(self): 122 | """Return true if device is on.""" 123 | status = self.get_status(self.coordinator.data) 124 | # _LOGGER.error(f"is on {status}") 125 | self._state = bool(int(status.get(FAN_POWER, 0))) 126 | 127 | return self._state 128 | 129 | @property 130 | def percentage(self) -> int | None: 131 | """Return the current speed.""" 132 | status = self.get_status(self.coordinator.data) 133 | 134 | is_on = bool(int(status.get(FAN_POWER, 0))) 135 | 136 | value = 0 137 | if is_on: 138 | if status.get(FAN_OPERATING_MODE, None) is None: 139 | _LOGGER.error("Can not get status!") 140 | return 0 141 | 142 | if self._device_type == DEVICE_TYPE_FAN: 143 | value = int(status.get(FAN_SPEED)) 144 | if self._device_type == DEVICE_TYPE_AIRPURIFIER: 145 | value = int( 146 | status.get(AIRPURIFIER_OPERATING_MODE) * self.percentage_step) 147 | return value 148 | return value 149 | 150 | async def async_turn_on( 151 | self, 152 | speed: str = None, 153 | percentage: int = None, 154 | preset_mode: str = None, 155 | **kwargs, 156 | ) -> None: 157 | """Turn the device on.""" 158 | gwid = self.device_gwid 159 | device_id = self.device_id 160 | if preset_mode: 161 | # If operation mode was set the device must not be turned on. 162 | await self.async_set_preset_mode(preset_mode) 163 | else: 164 | await self.client.set_device(gwid, device_id, FAN_POWER, 1) 165 | await asyncio.sleep(1) 166 | await self.client.update_device(gwid, device_id) 167 | self.async_write_ha_state() 168 | 169 | async def async_turn_off(self, **kwargs) -> None: 170 | """Turn the device off.""" 171 | gwid = self.device_gwid 172 | device_id = self.device_id 173 | await self.client.set_device(gwid, device_id, FAN_POWER, 0) 174 | await asyncio.sleep(1) 175 | await self.client.update_device(gwid, device_id) 176 | self.async_write_ha_state() 177 | 178 | @property 179 | def preset_modes(self) -> list[str] | None: 180 | """Get the list of available preset modes.""" 181 | modes = ["None"] 182 | if self._device_type == DEVICE_TYPE_FAN: 183 | modes = list(FAN_PRESET_MODES.keys()) 184 | if self._device_type == DEVICE_TYPE_AIRPURIFIER: 185 | modes.append(AIRPURIFIER_PRESET_MODES[AIRPURIFIER_NANOEX]) 186 | return modes 187 | 188 | @property 189 | def preset_mode(self) -> str | None: 190 | """Get the current preset mode.""" 191 | preset_mode = None 192 | status = self.get_status(self.coordinator.data) 193 | if self._device_type == DEVICE_TYPE_FAN: 194 | value = status.get(FAN_OPERATING_MODE, 0) 195 | preset_mode = get_key_from_dict(FAN_PRESET_MODES, value) 196 | if self._device_type == DEVICE_TYPE_AIRPURIFIER: 197 | value = status.get(AIRPURIFIER_NANOEX, 0) 198 | if value != 0: 199 | return AIRPURIFIER_PRESET_MODES[AIRPURIFIER_NANOEX] 200 | return preset_mode 201 | 202 | async def async_set_preset_mode(self, preset_mode: str) -> None: 203 | """Set the preset mode of the fan.""" 204 | gwid = self.device_gwid 205 | device_id = self.device_id 206 | if self._device_type == DEVICE_TYPE_FAN: 207 | await self.client.set_device( 208 | gwid, device_id, FAN_OPERATING_MODE, FAN_PRESET_MODES[preset_mode], 1) 209 | if self._device_type == DEVICE_TYPE_AIRPURIFIER: 210 | if preset_mode == AIRPURIFIER_NANOEX_PRESET: 211 | await self.client.set_device( 212 | gwid, device_id, AIRPURIFIER_NANOEX, 1 213 | ) 214 | elif preset_mode == "None": 215 | await self.client.set_device( 216 | gwid, device_id, AIRPURIFIER_NANOEX, 0 217 | ) 218 | # await self.client.set_device( 219 | # gwid, device_id, FAN_OPERATING_MODE, AIRPURIFIER_PRESET_MODES[preset_mode]) 220 | await self.client.update_device(gwid, device_id) 221 | self.async_write_ha_state() 222 | 223 | async def async_set_percentage(self, percentage: int) -> None: 224 | """Set the speed percentage of the fan.""" 225 | gwid = self.device_gwid 226 | device_id = self.device_id 227 | if percentage == 0: 228 | await self.client.set_device(gwid, device_id, FAN_POWER, 0) 229 | else: 230 | if self._device_type == DEVICE_TYPE_FAN: 231 | await self.client.set_device(gwid, device_id, FAN_SPEED, percentage) 232 | if self._device_type == DEVICE_TYPE_AIRPURIFIER: 233 | await self.client.set_device( 234 | gwid, device_id, AIRPURIFIER_OPERATING_MODE, percentage / self.percentage_step) 235 | await self.client.update_device(gwid, device_id) 236 | self.async_write_ha_state() 237 | 238 | @property 239 | def oscillating(self) -> bool | None: 240 | """Return the oscillation state.""" 241 | status = self.get_status(self.coordinator.data) 242 | value = False 243 | if self._device_type == DEVICE_TYPE_FAN: 244 | value = bool(status.get(FAN_OSCILLATE, 0)) 245 | 246 | return value 247 | 248 | async def async_oscillate(self, oscillating: bool) -> None: 249 | """Set oscillation.""" 250 | gwid = self.device_gwid 251 | device_id = self.device_id 252 | if self._device_type == DEVICE_TYPE_FAN: 253 | await self.client.set_device(gwid, device_id, FAN_OSCILLATE, oscillating) 254 | await self.client.update_device(gwid, device_id) 255 | self.async_write_ha_state() 256 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /custom_components/panasonic_ems2/climate.py: -------------------------------------------------------------------------------- 1 | """ Panasonic Smart Home Climate""" 2 | import logging 3 | import asyncio 4 | 5 | from homeassistant.components.climate import ClimateEntityFeature, ClimateEntity 6 | from homeassistant.const import ( 7 | UnitOfTemperature, 8 | ATTR_TEMPERATURE, 9 | STATE_UNAVAILABLE 10 | ) 11 | from homeassistant.components.climate.const import ( 12 | HVACMode, 13 | PRESET_NONE, 14 | SWING_ON, 15 | SWING_OFF, 16 | SWING_BOTH, 17 | SWING_VERTICAL, 18 | SWING_HORIZONTAL 19 | ) 20 | 21 | from .core.base import PanasonicBaseEntity 22 | from .core.const import ( 23 | DOMAIN, 24 | DATA_CLIENT, 25 | DATA_COORDINATOR, 26 | DEVICE_TYPE_CLIMATE, 27 | DEVICE_TYPE_ERV, 28 | CLIMATE_AVAILABLE_FAN_MODES, 29 | CLIMATE_AVAILABLE_MODES, 30 | CLIMATE_AVAILABLE_PRESET_MODES, 31 | CLIMATE_FAN_SPEED, 32 | CLIMATE_OPERATING_MODE, 33 | CLIMATE_POWER, 34 | CLIMATE_MAXIMUM_TEMPERATURE, 35 | CLIMATE_MINIMUM_TEMPERATURE, 36 | CLIMATE_SWING_VERTICAL_LEVEL, 37 | CLIMATE_SWING_HORIZONTAL_LEVEL, 38 | CLIMATE_TARGET_TEMPERATURE, 39 | CLIMATE_TEMPERATURE_INDOOR, 40 | CLIMATE_TEMPERATURE_STEP, 41 | CLIMATE_PRESET_MODE, 42 | CLIMATE_SWING_MODE, 43 | ERV_POWER, 44 | ERV_FAN_SPEED, 45 | ERV_OPERATING_MODE, 46 | ERV_AVAILABLE_MODES, 47 | ERV_AVAILABLE_FAN_MODES, 48 | ERV_TARGET_TEMPERATURE, 49 | ERV_TEMPERATURE_IN, 50 | ERV_MINIMUM_TEMPERATURE, 51 | ERV_MAXIMUM_TEMPERATURE 52 | ) 53 | 54 | _LOGGER = logging.getLogger(__name__) 55 | 56 | PANASONIC_CLIMATE_TYPE = [ 57 | str(DEVICE_TYPE_CLIMATE), 58 | str(DEVICE_TYPE_ERV) 59 | ] 60 | 61 | def get_key_from_dict(dictionary, value): 62 | """ get key from dictionary by value""" 63 | for key, val in dictionary.items(): 64 | if value == val: 65 | return key 66 | return None 67 | 68 | async def async_setup_entry(hass, entry, async_add_entities) -> bool: 69 | client = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] 70 | coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] 71 | devices = coordinator.data 72 | climate = [] 73 | 74 | for device_gwid, info in devices.items(): 75 | device_type = info.get("DeviceType", None) 76 | if (device_type and 77 | str(device_type) in PANASONIC_CLIMATE_TYPE): 78 | if not client.is_supported(info.get("ModelType", "")): 79 | continue 80 | for dev in info["Devices"]: 81 | device_id = dev["DeviceID"] 82 | climate.append( 83 | PanasonicClimate( 84 | coordinator, 85 | device_gwid, 86 | device_id, 87 | client, 88 | info, 89 | ) 90 | ) 91 | 92 | async_add_entities(climate, True) 93 | 94 | return True 95 | 96 | 97 | class PanasonicClimate(PanasonicBaseEntity, ClimateEntity): 98 | _swing_mode = SWING_OFF 99 | 100 | def __init__( 101 | self, 102 | coordinator, 103 | device_gwid, 104 | device_id, 105 | client, 106 | info, 107 | ): 108 | super().__init__(coordinator, device_gwid, device_id, client, info) 109 | device_type = info.get("DeviceType", None) 110 | self._device_type = int(device_type) 111 | 112 | @property 113 | def supported_features(self) -> int: 114 | """Return the list of supported features.""" 115 | features = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON 116 | status = self.get_status(self.coordinator.data) 117 | 118 | preset_mode = False 119 | if self._device_type == DEVICE_TYPE_CLIMATE: 120 | if (status.get(CLIMATE_SWING_VERTICAL_LEVEL, None) is not None and 121 | (status.get(CLIMATE_SWING_HORIZONTAL_LEVEL, None) is not None)): 122 | features |= ClimateEntityFeature.SWING_MODE 123 | 124 | if status.get(CLIMATE_FAN_SPEED, None) is not None: 125 | features |= ClimateEntityFeature.FAN_MODE 126 | 127 | for st in status: 128 | if st in CLIMATE_AVAILABLE_PRESET_MODES: 129 | preset_mode = True 130 | break 131 | 132 | elif self._device_type == DEVICE_TYPE_ERV: 133 | if status.get(ERV_FAN_SPEED, None) is not None: 134 | features |= ClimateEntityFeature.FAN_MODE 135 | 136 | if preset_mode: 137 | features |= ClimateEntityFeature.PRESET_MODE 138 | 139 | return features 140 | 141 | @property 142 | def temperature_unit(self) -> str: 143 | return UnitOfTemperature.CELSIUS 144 | 145 | @property 146 | def hvac_mode(self) -> str: 147 | """Return hvac operation ie. heat, cool mode.""" 148 | status = self.get_status(self.coordinator.data) 149 | if self._device_type == DEVICE_TYPE_ERV: 150 | power = ERV_POWER 151 | operation_mode = ERV_OPERATING_MODE 152 | available_modes = ERV_AVAILABLE_MODES 153 | else: 154 | power = CLIMATE_POWER 155 | operation_mode = CLIMATE_OPERATING_MODE 156 | available_modes = CLIMATE_AVAILABLE_MODES 157 | 158 | is_on = bool(int(status.get(power, 0))) 159 | 160 | if is_on: 161 | if status.get(operation_mode, None) is None: 162 | _LOGGER.error("Can not get status!") 163 | return HVACMode.OFF 164 | value = status.get(operation_mode, None) 165 | #if value is None: 166 | # return STATE_UNAVAILABLE 167 | return get_key_from_dict(available_modes, int(value)) 168 | 169 | return HVACMode.OFF 170 | 171 | @property 172 | def hvac_modes(self) -> list: 173 | """Return the list of available hvac operation modes.""" 174 | hvac_modes = [HVACMode.OFF] 175 | available_modes = {} 176 | 177 | if self._device_type == DEVICE_TYPE_ERV: 178 | rng = self.client.get_range(self.device_gwid, ERV_OPERATING_MODE) 179 | available_modes = ERV_AVAILABLE_MODES 180 | else: 181 | rng = self.client.get_range(self.device_gwid, CLIMATE_OPERATING_MODE) 182 | available_modes = CLIMATE_AVAILABLE_MODES 183 | 184 | for mode, value in available_modes.items(): 185 | if value >= 0: 186 | for _, value2 in rng.items(): 187 | if value == value2: 188 | hvac_modes.append(mode) 189 | break 190 | return hvac_modes 191 | 192 | async def async_set_hvac_mode(self, hvac_mode) -> None: 193 | """Set new target hvac mode.""" 194 | status = self.get_status(self.coordinator.data) 195 | if self._device_type == DEVICE_TYPE_ERV: 196 | power = ERV_POWER 197 | operation_mode = ERV_OPERATING_MODE 198 | else: 199 | power = CLIMATE_POWER 200 | operation_mode = CLIMATE_OPERATING_MODE 201 | 202 | is_on = bool(int(status.get(power, 0))) 203 | gwid = self.device_gwid 204 | device_id = self.device_id 205 | if hvac_mode == HVACMode.OFF: 206 | await self.client.set_device(gwid, device_id, power, 0) 207 | else: 208 | mode = CLIMATE_AVAILABLE_MODES.get(hvac_mode) 209 | await self.client.set_device(gwid, device_id, operation_mode, mode) 210 | if not is_on: 211 | await self.client.set_device(gwid, device_id, power, 1) 212 | 213 | await asyncio.sleep(1) 214 | await self.client.update_device(gwid, device_id) 215 | self.async_write_ha_state() 216 | 217 | @property 218 | def preset_mode(self) -> str: 219 | """Return the current preset mode, e.g., home, away, temp.""" 220 | status = self.get_status(self.coordinator.data) 221 | 222 | is_on = status.get(CLIMATE_POWER, None) 223 | #if is_on is None: 224 | # return STATE_UNAVAILABLE 225 | preset_mode = PRESET_NONE 226 | for key, mode in CLIMATE_AVAILABLE_PRESET_MODES.items(): 227 | if key in status and status[key]: 228 | preset_mode = mode 229 | break 230 | 231 | preset_mode = preset_mode if bool(int(is_on)) else PRESET_NONE 232 | 233 | return preset_mode 234 | 235 | @property 236 | def preset_modes(self) -> list: 237 | """Return a list of available preset modes.""" 238 | status = self.get_status(self.coordinator.data) 239 | modes = [PRESET_NONE] 240 | 241 | for mode in status: 242 | if mode in CLIMATE_AVAILABLE_PRESET_MODES: 243 | modes.append(CLIMATE_AVAILABLE_PRESET_MODES[mode]) 244 | 245 | return modes 246 | 247 | async def async_set_preset_mode(self, preset_mode) -> None: 248 | """Set new preset mode.""" 249 | status = self.get_status(self.coordinator.data) 250 | is_on = bool(status.get(CLIMATE_POWER, 0)) 251 | 252 | func = get_key_from_dict(CLIMATE_AVAILABLE_PRESET_MODES, preset_mode) 253 | gwid = self.device_gwid 254 | device_id = self.device_id 255 | 256 | await self.client.set_device(gwid, device_id, func, 1) 257 | if not is_on: 258 | await self.client.set_device(gwid, device_id, CLIMATE_POWER, 1) 259 | 260 | await self.client.update_device(gwid, device_id) 261 | self.async_write_ha_state() 262 | 263 | @property 264 | def fan_mode(self) -> str: 265 | """Return the fan setting.""" 266 | status = self.get_status(self.coordinator.data) 267 | if self._device_type == DEVICE_TYPE_ERV: 268 | fan_mode = ERV_OPERATING_MODE 269 | available_fan_modes = ERV_AVAILABLE_FAN_MODES 270 | else: 271 | fan_mode = CLIMATE_FAN_SPEED 272 | available_fan_modes = CLIMATE_AVAILABLE_FAN_MODES 273 | 274 | mode = status.get(fan_mode, None) 275 | #if fan_mode is None: 276 | # return STATE_UNAVAILABLE 277 | value = get_key_from_dict(available_fan_modes, int(mode)) 278 | 279 | return value 280 | 281 | @property 282 | def fan_modes(self) -> list: 283 | """Return the list of available fan modes.""" 284 | modes = [] 285 | if self._device_type == DEVICE_TYPE_ERV: 286 | fan_mode = ERV_OPERATING_MODE 287 | available_fan_modes = ERV_AVAILABLE_FAN_MODES 288 | else: 289 | fan_mode = CLIMATE_FAN_SPEED 290 | available_fan_modes = CLIMATE_AVAILABLE_FAN_MODES 291 | 292 | rng = self.client.get_range(self.device_gwid, fan_mode) 293 | if "Max" in rng: 294 | max = rng.get("Max", 1) 295 | 296 | for mode, value in available_fan_modes.items(): 297 | if max >= value: 298 | modes.append(mode) 299 | 300 | if len(modes) <= 1: 301 | modes.append("Auto") 302 | else: 303 | modes = list(rng.keys()) 304 | return modes 305 | 306 | async def async_set_fan_mode(self, mode) -> None: 307 | """Set new fan mode.""" 308 | if self._device_type == DEVICE_TYPE_ERV: 309 | fan_mode = ERV_OPERATING_MODE 310 | available_fan_modes = ERV_AVAILABLE_FAN_MODES 311 | else: 312 | fan_mode = CLIMATE_FAN_SPEED 313 | available_fan_modes = CLIMATE_AVAILABLE_FAN_MODES 314 | 315 | value = available_fan_modes[mode] 316 | gwid = self.device_gwid 317 | device_id = self.device_id 318 | 319 | await self.client.set_device(gwid, device_id, fan_mode, value) 320 | await self.client.update_device(gwid, device_id) 321 | self.async_write_ha_state() 322 | 323 | @property 324 | def swing_mode(self) -> str: 325 | """Return the swing setting.""" 326 | status = self.get_status(self.coordinator.data) 327 | 328 | swing_vertical = status.get(CLIMATE_SWING_VERTICAL_LEVEL, None) 329 | swing_horizontal = status.get(CLIMATE_SWING_HORIZONTAL_LEVEL, None) 330 | #if swing_horizontal is None or swing_vertical is None: 331 | # return STATE_UNAVAILABLE 332 | swing_vertical = bool(swing_vertical) 333 | swing_horizontal = bool(swing_horizontal) 334 | mode = SWING_OFF 335 | if swing_vertical or swing_horizontal: 336 | mode = SWING_ON 337 | 338 | if swing_vertical and swing_horizontal: 339 | mode = SWING_BOTH 340 | 341 | elif swing_vertical: 342 | mode = SWING_VERTICAL 343 | 344 | elif swing_horizontal: 345 | mode = SWING_HORIZONTAL 346 | 347 | self._swing_mode = mode 348 | return mode 349 | 350 | @property 351 | def swing_modes(self) -> list: 352 | """Return the list of available swing modes. 353 | 354 | Requires ClimateEntityFeature.SWING_MODE. 355 | """ 356 | status = self.get_status(self.coordinator.data) 357 | swing_modes = [SWING_ON, SWING_OFF] 358 | 359 | swing_vertical = status.get(CLIMATE_SWING_VERTICAL_LEVEL, None) 360 | if swing_vertical is not None: 361 | swing_modes.append(SWING_VERTICAL) 362 | 363 | swing_horizontal = status.get(CLIMATE_SWING_HORIZONTAL_LEVEL, None) 364 | if swing_horizontal is not None: 365 | swing_modes.append(SWING_HORIZONTAL) 366 | 367 | if swing_vertical is not None and swing_horizontal is not None: 368 | swing_modes.append(SWING_BOTH) 369 | 370 | return swing_modes 371 | 372 | async def async_set_swing_mode(self, swing_mode) -> None: 373 | """Set new target swing operation.""" 374 | gwid = self.device_gwid 375 | device_id = self.device_id 376 | 377 | if swing_mode == SWING_ON: 378 | if self._swing_mode == SWING_HORIZONTAL: 379 | mode = 1 380 | if self._swing_mode == SWING_VERTICAL: 381 | mode = 2 382 | 383 | elif swing_mode == SWING_OFF: 384 | mode = 0 385 | 386 | elif swing_mode == SWING_HORIZONTAL: 387 | mode = 1 388 | elif swing_mode == SWING_VERTICAL: 389 | mode = 2 390 | 391 | elif swing_mode == SWING_BOTH: 392 | mode = 4 393 | 394 | await self.client.set_device(gwid, device_id, CLIMATE_SWING_MODE, mode) 395 | await self.client.update_device(gwid, device_id) 396 | self.async_write_ha_state() 397 | 398 | @property 399 | def target_temperature(self) -> int: 400 | """Return the temperature we try to reach.""" 401 | status = self.get_status(self.coordinator.data) 402 | if self._device_type == DEVICE_TYPE_ERV: 403 | return float(status.get(ERV_TARGET_TEMPERATURE, 0)) 404 | else: 405 | return float(status.get(CLIMATE_TARGET_TEMPERATURE, 0)) 406 | 407 | async def async_set_temperature(self, **kwargs): 408 | """ Set new target temperature """ 409 | temp = kwargs.get(ATTR_TEMPERATURE) 410 | gwid = self.device_gwid 411 | device_id = self.device_id 412 | if self._device_type == DEVICE_TYPE_ERV: 413 | await self.client.set_device(gwid, device_id, ERV_TARGET_TEMPERATURE, int(temp)) 414 | else: 415 | await self.client.set_device(gwid, device_id, CLIMATE_TARGET_TEMPERATURE, int(temp)) 416 | await self.client.update_device(gwid, device_id) 417 | self.async_write_ha_state() 418 | 419 | @property 420 | def current_temperature(self) -> int: 421 | """Return the current temperature.""" 422 | status = self.get_status(self.coordinator.data) 423 | if self._device_type == DEVICE_TYPE_ERV: 424 | return float(status.get(ERV_TEMPERATURE_IN, 0)) 425 | else: 426 | return float(status.get(CLIMATE_TEMPERATURE_INDOOR, 0)) 427 | 428 | @property 429 | def min_temp(self) -> int: 430 | """ Return the minimum temperature """ 431 | if self._device_type == DEVICE_TYPE_ERV: 432 | rng = self.client.get_range(self.device_gwid, ERV_TARGET_TEMPERATURE) 433 | return rng.get("Min", ERV_MINIMUM_TEMPERATURE) 434 | 435 | rng = self.client.get_range(self.device_gwid, CLIMATE_TARGET_TEMPERATURE) 436 | return rng.get("Min", CLIMATE_MINIMUM_TEMPERATURE) 437 | 438 | @property 439 | def max_temp(self) -> int: 440 | """ Return the maximum temperature """ 441 | if self._device_type == DEVICE_TYPE_ERV: 442 | rng = self.client.get_range(self.device_gwid, ERV_TARGET_TEMPERATURE) 443 | return rng.get("Max", ERV_MAXIMUM_TEMPERATURE) 444 | 445 | rng = self.client.get_range(self.device_gwid, CLIMATE_TARGET_TEMPERATURE) 446 | return rng.get("Max", CLIMATE_MAXIMUM_TEMPERATURE) 447 | 448 | @property 449 | def target_temperature_step(self) -> float: 450 | """ Return temperature step """ 451 | return CLIMATE_TEMPERATURE_STEP -------------------------------------------------------------------------------- /custom_components/panasonic_ems2/core/cloud.py: -------------------------------------------------------------------------------- 1 | """ Panasonic Smart Home """ 2 | import logging 3 | import asyncio 4 | from http import HTTPStatus 5 | import requests 6 | import json 7 | from datetime import datetime 8 | import pytz 9 | from typing import Literal 10 | 11 | from homeassistant.helpers.update_coordinator import UpdateFailed 12 | from homeassistant.helpers.storage import Store 13 | from homeassistant.const import CONTENT_TYPE_JSON, EVENT_HOMEASSISTANT_STOP 14 | 15 | from .exceptions import ( 16 | Ems2TokenNotFound, 17 | Ems2LoginFailed, 18 | Ems2ExceedRateLimit, 19 | Ems2Expectation, 20 | Ems2TooManyRequest 21 | ) 22 | from . import apis 23 | from .const import ( 24 | APP_TOKEN, 25 | DOMAIN, 26 | CONF_CPTOKEN, 27 | CONF_TOKEN_TIMEOUT, 28 | CONF_REFRESH_TOKEN, 29 | CONF_REFRESH_TOKEN_TIMEOUT, 30 | COMMANDS_TYPE, 31 | EXTRA_COMMANDS, 32 | EXCESS_COMMANDS, 33 | MODEL_JP_TYPES, 34 | CLIMATE_PM25, 35 | DEVICE_TYPE_DEHUMIDIFIER, 36 | DEVICE_TYPE_FRIDGE, 37 | DEVICE_TYPE_LIGHT, 38 | DEVICE_TYPE_WASHING_MACHINE, 39 | DEVICE_TYPE_WEIGHT_PLATE, 40 | ENTITY_MONTHLY_ENERGY, 41 | ENTITY_DOOR_OPENS, 42 | ENTITY_WASH_TIMES, 43 | ENTITY_WATER_USED, 44 | ENTITY_UPDATE, 45 | ENTITY_UPDATE_INFO, 46 | DEHUMIDIFIER_PM25, 47 | FRIDGE_FREEZER_TEMPERATURE, 48 | FRIDGE_THAW_TEMPERATURE, 49 | FRIDGE_XGS_COMMANDS, 50 | HA_USER_AGENT, 51 | LIGHT_OPERATION_STATE, 52 | LIGHT_CHANNEL_1_TIMER_ON, 53 | LIGHT_CHANNEL_1_TIMER_OFF, 54 | LIGHT_CHANNEL_2_TIMER_ON, 55 | LIGHT_CHANNEL_2_TIMER_OFF, 56 | LIGHT_CHANNEL_3_TIMER_ON, 57 | LIGHT_CHANNEL_3_TIMER_OFF, 58 | WASHING_MACHINE_MODELS, 59 | WASHING_MACHINE_2020_MODELS, 60 | WASHING_MACHINE_OPERATING_STATUS, 61 | WASHING_MACHINE_POSTPONE_DRYING, 62 | WASHING_MACHINE_TIMER_REMAINING_TIME, 63 | WASHING_MACHINE_PROGRESS, 64 | WEIGHT_PLATE_FOOD_NAME, 65 | WEIGHT_PLATE_MANAGEMENT_MODE, 66 | WEIGHT_PLATE_MANAGEMENT_VALUE, 67 | WEIGHT_PLATE_AMOUNT_MAX, 68 | WEIGHT_PLATE_BUY_DATE, 69 | WEIGHT_PLATE_DUE_DATE, 70 | WEIGHT_PLATE_COMMUNICATION_MODE, 71 | WEIGHT_PLATE_COMMUNICATION_TIME, 72 | WEIGHT_PLATE_TOTAL_WEIGHT, 73 | WEIGHT_PLATE_RESTORE_WEIGHT, 74 | WEIGHT_PLATE_LOW_BATTERY, 75 | SET_COMMAND_TYPE, 76 | USER_INFO_TYPES, 77 | REQUEST_TIMEOUT 78 | ) 79 | local_tz = pytz.timezone('Asia/Taipei') 80 | 81 | _LOGGER = logging.getLogger(__name__) 82 | 83 | def api_status(func): 84 | """ 85 | wrapper_call 86 | """ 87 | async def wrapper_call(*args, **kwargs): 88 | try: 89 | return await func(*args, **kwargs) 90 | except Ems2TokenNotFound: 91 | await args[0].refresh_token() 92 | return await func(*args, **kwargs) 93 | except Ems2LoginFailed: 94 | await args[0].login() 95 | return await func(*args, **kwargs) 96 | except Ems2TooManyRequest: 97 | await asyncio.sleep(2) 98 | return await func(*args, **kwargs) 99 | except Ems2Expectation: 100 | return args[0]._devices_info 101 | except ( 102 | Exception, 103 | ) as e: 104 | _LOGGER.warning(f"Got exception {e}") 105 | #return {} 106 | return args[0]._devices_info 107 | return wrapper_call 108 | 109 | 110 | class PanasonicSmartHome(object): 111 | """ 112 | Panasonic Smart Home Object 113 | """ 114 | def __init__(self, hass, session, account, password): 115 | self.hass = hass 116 | self.email = account 117 | self.password = password 118 | self._session = session 119 | self._devices = [] 120 | self._select_devices = [] 121 | self._commands = [] 122 | self._devices_info = {} 123 | self._commands_info = {} 124 | self._update_info = {} 125 | self._cp_token = "" 126 | self._refresh_token = None 127 | self._expires_in = 0 128 | self._expire_time = None 129 | self._token_timeout = None 130 | self._refresh_token_timeout = None 131 | self._mversion = None 132 | self._update_timestamp = None 133 | self._api_counts = 0 134 | self._api_counts_per_hour = 0 135 | 136 | async def request( 137 | self, 138 | method: Literal["GET", "POST"], 139 | headers, 140 | endpoint: str, 141 | params=None, 142 | data=None, 143 | ): 144 | """Shared request method""" 145 | res = {} 146 | headers["user-agent"] = HA_USER_AGENT 147 | headers["Content-Type"] = CONTENT_TYPE_JSON 148 | 149 | self._api_counts = self._api_counts + 1 150 | self._api_counts_per_hour = self._api_counts_per_hour + 1 151 | try: 152 | response = await self._session.request( 153 | method, 154 | url=endpoint, 155 | json=data, 156 | params=params, 157 | headers=headers, 158 | timeout=REQUEST_TIMEOUT 159 | ) 160 | except requests.exceptions.RequestException: 161 | _LOGGER.error(f"Failed fetching data for {self.email}") 162 | return {} 163 | except Exception as e: 164 | # request timeout 165 | # _LOGGER.error(f"{endpoint} {headers['GWID']} request exception {e}, timeout?") 166 | return {} 167 | 168 | if response.status == HTTPStatus.OK: 169 | try: 170 | res = await response.json() 171 | except: 172 | res = response.text 173 | elif response.status == HTTPStatus.BAD_REQUEST: 174 | raise Ems2ExceedRateLimit 175 | elif response.status == HTTPStatus.FORBIDDEN: 176 | raise Ems2LoginFailed 177 | elif response.status == HTTPStatus.TOO_MANY_REQUESTS: 178 | raise Ems2TooManyRequest 179 | elif response.status == HTTPStatus.EXPECTATION_FAILED: 180 | raise Ems2Expectation 181 | elif response.status == HTTPStatus.NOT_FOUND: 182 | _LOGGER.warning(f"Use wrong method or parameters") 183 | res = {} 184 | elif response.status == HTTPStatus.METHOD_NOT_ALLOWED: 185 | _LOGGER.warning(f"The method is not allowed") 186 | res = {} 187 | elif response.status == 429: 188 | _LOGGER.warning(f"Wrong") 189 | res = {} 190 | else: 191 | _LOGGER.error(f"request {response}") 192 | raise Ems2TokenNotFound 193 | 194 | if isinstance(res, str): 195 | return {"data": res} 196 | 197 | if isinstance(res, list): 198 | return {"data": res} 199 | 200 | if isinstance(res, dict): 201 | return res 202 | 203 | return res 204 | 205 | @property 206 | def token(self) -> bool: 207 | if len(self._cp_token) >= 1: 208 | return True 209 | return False 210 | 211 | @property 212 | def devices_number(self) -> int: 213 | return len(self._devices) 214 | 215 | async def set_select_devices(self, devices): 216 | """ 217 | set select devices 218 | """ 219 | self._select_devices = list(devices.values()) 220 | 221 | async def get_user_accounts_number(self): 222 | """ 223 | get the number of user accounts 224 | """ 225 | accounts = 0 226 | store = Store(self.hass, 1, f"{DOMAIN}/tokens.json") 227 | data = await store.async_load() or None 228 | if not data: 229 | return 1 230 | 231 | for _, value in data.items(): 232 | token_timeout = value[CONF_TOKEN_TIMEOUT] 233 | now = datetime.now() 234 | timeout = datetime( 235 | int(token_timeout[:4]), 236 | int(token_timeout[4:6]), 237 | int(token_timeout[6:8]), 238 | int(token_timeout[8:10]), 239 | int(token_timeout[10:12]), 240 | int(token_timeout[12:]) 241 | ) 242 | 243 | if int(timeout.timestamp() - now.timestamp()) > 0: 244 | accounts = accounts + 1 245 | 246 | return accounts 247 | 248 | @api_status 249 | async def login(self): 250 | """ 251 | Login to get access token. 252 | """ 253 | data = {"MemId": self.email, "PW": self.password, "AppToken": APP_TOKEN} 254 | response = await self.request( 255 | method="POST", headers={}, data=data, endpoint=apis.open_session() 256 | ) 257 | self._cp_token = response.get("CPToken", "") 258 | self._refresh_token = response.get("RefreshToken", "") 259 | self._token_timeout = response.get("TokenTimeOut", "") 260 | self._refresh_token_timeout = response.get("RefreshTokenTimeOut", "") 261 | self._mversion = response.get("MVersion", "") 262 | 263 | @api_status 264 | async def refresh_token(self): 265 | """ 266 | refresh access token. 267 | """ 268 | if self._refresh_token is None: 269 | raise Ems2LoginFailed 270 | 271 | data = {"RefreshToken": self._refresh_token} 272 | response = await self.request( 273 | method="POST", headers={}, data=data, endpoint=apis.refresh_token() 274 | ) 275 | self._cp_token = response.get("CPToken", "") 276 | self._refresh_token = response.get("RefreshToken", "") 277 | self._token_timeout = response.get("TokenTimeOut", "") 278 | self._refresh_token_timeout = response.get("RefreshTokenTimeOut", "") 279 | self._mversion = response.get("MVersion", "") 280 | 281 | @api_status 282 | async def logout(self): 283 | """ 284 | Logout the account 285 | """ 286 | data = {} 287 | await self.request( 288 | method="POST", headers={}, data=data, endpoint=apis.close_session() 289 | ) 290 | 291 | @api_status 292 | async def get_user_devices(self): 293 | """ 294 | List devices that the user has permission 295 | """ 296 | 297 | header = {"CPToken": self._cp_token} 298 | response = await self.request( 299 | method="GET", headers=header, endpoint=apis.get_user_devices() 300 | ) 301 | if isinstance(response, dict): 302 | self._devices = response.get("GwList", []) 303 | self._commands = response.get("CommandList", []) 304 | 305 | return self._devices 306 | 307 | @api_status 308 | async def get_device_ip(self): 309 | """ 310 | Get the ip of devices 311 | """ 312 | idx = 0 313 | header = {"CPToken": self._cp_token} 314 | for device in self._devices: 315 | asyncio.sleep(.5) # avoid to be banned 316 | gwid = device["GWID"] 317 | data = {"GWID": gwid} 318 | response = await self.request( 319 | method="POST", headers=header, data=data, endpoint=apis.get_gw_ip() 320 | ) 321 | if isinstance(response, dict): 322 | self._devices[idx]["GWIP"] = response.get("data", None) 323 | idx = idx + 1 324 | 325 | def _workaround_info(self, model_type: str, command_type: str, status): 326 | """ 327 | some workaround on info 328 | """ 329 | try: 330 | new = int(status) 331 | if ("RX" in model_type and 332 | command_type == CLIMATE_PM25 and 333 | int(status) == 65535 334 | ): 335 | new = -1 336 | elif (model_type in ["HDH", "KBS", "LMS", "LM", "DDH", "MDH", "DW", "LX128B"] and 337 | command_type == WASHING_MACHINE_TIMER_REMAINING_TIME 338 | ): 339 | if int(status) > 65000: 340 | new = 0 341 | elif (model_type in ["XGS"] and 342 | command_type in [ 343 | FRIDGE_FREEZER_TEMPERATURE, 344 | FRIDGE_THAW_TEMPERATURE 345 | ] 346 | ): 347 | new = int(status) - 255 348 | elif ((model_type in ["GXW", "JHW"]) and 349 | command_type == DEHUMIDIFIER_PM25 and 350 | int(status) == 65535 351 | ): 352 | new = -1 353 | except: 354 | new = status 355 | return command_type, new 356 | 357 | def _refactor_info(self, model_type: str, devices_info: list): 358 | """ 359 | refactor the status of information for easy use 360 | """ 361 | if len(devices_info) < 1: 362 | return [] 363 | 364 | new = [] 365 | for device in devices_info: 366 | device_id = device.get("DeviceID", None) 367 | if device_id is not None: 368 | device_info = device["Info"] 369 | device_status = {} 370 | for info in device_info: 371 | command_type, status = self._workaround_info( 372 | model_type, 373 | info["CommandType"], 374 | info["status"] 375 | ) 376 | device_status[command_type] = status 377 | device["status"] = device_status 378 | device.pop("Info", None) 379 | new.append(device) 380 | return new 381 | 382 | @api_status 383 | async def get_device_with_info(self, device: dict, func: list): 384 | """ 385 | Get device information 386 | """ 387 | gwid = device["GWID"] 388 | if not gwid: 389 | _LOGGER.warning("GWID is not exist!") 390 | return {} 391 | 392 | header = { 393 | "CPToken": self._cp_token, 394 | "auth": device["Auth"], 395 | "GWID": gwid 396 | } 397 | data = [] 398 | device_func = [] 399 | for dev in device["Devices"]: 400 | if dev: 401 | device_id = dev.get("DeviceID", 1) 402 | device_func = self._get_device_commands( 403 | device["DeviceType"], 404 | device["ModelType"], 405 | device["Model"], 406 | device_id 407 | ) 408 | device_func.extend(func) 409 | data.append( 410 | {"CommandTypes": device_func, "DeviceID": device_id} 411 | ) 412 | response = await self.request( 413 | method="POST", headers=header, data=data, endpoint=apis.post_device_get_info() 414 | ) 415 | 416 | info = [] 417 | if response.get("status", "") == "success": 418 | info = self._refactor_info( 419 | self._devices_info[gwid]["ModelType"], 420 | response["devices"] 421 | ) 422 | 423 | if len(info) >= 1: 424 | self._devices_info[gwid]["Information"] = info 425 | return info 426 | 427 | def _get_commands(self, device_type, model_type, model): 428 | """ 429 | get commands (saa: service code) 430 | """ 431 | cmds_list = [ 432 | {"CommandType": "0x00"} 433 | ] 434 | if not self._commands: 435 | return cmds_list 436 | commands_type = [] 437 | cmds = COMMANDS_TYPE.get(str(device_type), cmds_list) 438 | extra_cmds = EXTRA_COMMANDS.get(str(device_type), {}).get(model_type, []) 439 | excess_cmds = EXCESS_COMMANDS.get(str(device_type), {}).get(model_type, []) 440 | if (int(device_type) == DEVICE_TYPE_FRIDGE and 441 | model_type not in MODEL_JP_TYPES and 442 | len(extra_cmds) < 1 443 | ): 444 | new_cmds = cmds + extra_cmds + FRIDGE_XGS_COMMANDS 445 | else: 446 | new_cmds = cmds + extra_cmds 447 | 448 | if len(excess_cmds) >= 1: 449 | for cmd in excess_cmds: 450 | if cmd in new_cmds: 451 | new_cmds.remove(cmd) 452 | 453 | for cmd in new_cmds: 454 | commands_type.append( 455 | {"CommandType": cmd} 456 | ) 457 | 458 | return commands_type 459 | 460 | def _get_device_commands(self, device_type, model_type, model, device_id): 461 | """ 462 | get commands (saa: service code) 463 | """ 464 | new_cmds = [] 465 | if (int(device_type) == DEVICE_TYPE_LIGHT): 466 | if ((model in ["F540107", "F241107", "F540207", "F540207"]) and (device_id == 1)): 467 | new_cmds.extend([LIGHT_CHANNEL_1_TIMER_ON, LIGHT_CHANNEL_1_TIMER_OFF, LIGHT_OPERATION_STATE]) 468 | elif ((model in ["F540207", "F540207"]) and (device_id == 2)): 469 | new_cmds.extend([LIGHT_CHANNEL_2_TIMER_ON, LIGHT_CHANNEL_2_TIMER_OFF]) 470 | elif ((model == "F540307") and (device_id == 3)): 471 | new_cmds.extend([LIGHT_CHANNEL_3_TIMER_ON, LIGHT_CHANNEL_3_TIMER_OFF]) 472 | commands_type = [] 473 | for cmd in new_cmds: 474 | commands_type.append( 475 | {"CommandType": cmd} 476 | ) 477 | 478 | return commands_type 479 | 480 | def _refactor_cmds_paras(self, commands_list: dict) -> list: 481 | """ 482 | refactor the status of information for easy use 483 | """ 484 | new = {} 485 | for model_type, cmd_list in commands_list.items(): 486 | cmds_list = [] 487 | for cmds in cmd_list: 488 | if "list" not in cmds: 489 | continue 490 | lst = cmds["list"] 491 | cmds_para = {} 492 | cmds_name = {} 493 | for cmd in lst: 494 | cmd_type = cmd["CommandType"].upper().replace("X", "x") 495 | parameters = {} 496 | if cmd["ParameterType"] == "enum": 497 | parameters_list = cmd["Parameters"] 498 | for para in parameters_list: 499 | parameters[para[0]] = para[1] 500 | if model_type in WASHING_MACHINE_MODELS + WASHING_MACHINE_2020_MODELS: 501 | if cmd_type == WASHING_MACHINE_OPERATING_STATUS: 502 | parameters["Off"] = 0 503 | elif "range" in cmd["ParameterType"]: 504 | parameters_list = cmd["Parameters"] 505 | for para in parameters_list: 506 | if "Min" == para[0]: 507 | min = para[1] or 0 508 | if "Max" == para[0]: 509 | max = para[1] or 1 510 | if max > 39: 511 | parameters[str(min)] = min 512 | parameters[str(max)] = max 513 | else: 514 | for i in range(min, max + 1): 515 | parameters[str(i)] = i 516 | if cmd["ParameterType"] == "rangeA": 517 | parameters["Auto"] = 0 518 | 519 | if model_type in WASHING_MACHINE_MODELS + WASHING_MACHINE_2020_MODELS: 520 | if cmd_type == "0x61": 521 | parameters["Off"] = 0 522 | cmds_para[WASHING_MACHINE_POSTPONE_DRYING] = parameters 523 | cmds_name[WASHING_MACHINE_POSTPONE_DRYING] = cmd["CommandName"] 524 | if cmd_type == "0x15": 525 | cmds_para[WASHING_MACHINE_TIMER_REMAINING_TIME] = parameters 526 | cmds_name[WASHING_MACHINE_TIMER_REMAINING_TIME] = cmd["CommandName"] 527 | 528 | cmds_para[cmd_type] = parameters 529 | cmds_name[cmd_type] = cmd["CommandName"] 530 | cmds.pop("list", None) 531 | cmds["DeviceType"] = str(cmds["DeviceType"]) 532 | cmds["CommandParameters"] = cmds_para 533 | cmds["CommandName"] = cmds_name 534 | cmds_list.append(cmds) 535 | new[model_type] = cmds_list 536 | self._commands_info = new 537 | 538 | def _offline_info(self, device_type, model_type): 539 | """ 540 | For washing machine, can not get info after offline 541 | 542 | Returns: 543 | list: the info of device 544 | """ 545 | commands = COMMANDS_TYPE.get(str(device_type), None) 546 | extra_cmds = EXTRA_COMMANDS.get(str(device_type), {}).get(model_type, []) 547 | status = {} 548 | if commands: 549 | for key in commands + extra_cmds: 550 | status[key] = 0 551 | 552 | return [{'DeviceID': 1, 'status': status}] 553 | 554 | def is_supported(self, model_type: str): 555 | """is model type supported 556 | 557 | Args: 558 | model_type (str): return True if supported 559 | """ 560 | 561 | return True 562 | 563 | @api_status 564 | async def get_user_info(self): 565 | """ get user info 566 | 567 | Returns: 568 | bool: is user info got 569 | """ 570 | header = {"CPToken": self._cp_token} 571 | data = { 572 | "name": "", 573 | "from": datetime.today().replace(day=1).strftime("%Y/%m/%d"), 574 | "unit": "day", 575 | "max_num": 31, 576 | } 577 | for info in USER_INFO_TYPES: 578 | data["name"] = info 579 | response = await self.request( 580 | method="POST", headers=header, data=data, endpoint=apis.get_user_info() 581 | ) 582 | 583 | if "GwList" not in response: 584 | return False 585 | for gwinfo in response["GwList"]: 586 | gwid = gwinfo["GwID"] 587 | if "Information" not in self._devices_info.get(gwid, {}): 588 | continue 589 | device_type = self._devices_info[gwid]["DeviceType"] 590 | if info == "Other": 591 | if device_type == str(DEVICE_TYPE_FRIDGE): 592 | self._devices_info[gwid]["Information"][0]["status"][ENTITY_DOOR_OPENS] = gwinfo["Ref_OpenDoor_Total"] 593 | if device_type == str(DEVICE_TYPE_WASHING_MACHINE): 594 | self._devices_info[gwid]["Information"][0]["status"][ENTITY_WASH_TIMES] = gwinfo["WM_WashTime_Total"] 595 | self._devices_info[gwid]["Information"][0]["status"][ENTITY_WATER_USED] = gwinfo["WM_WaterUsed_Total"] 596 | if info == "Power": 597 | if device_type == str(DEVICE_TYPE_DEHUMIDIFIER): 598 | self._devices_info[gwid]["Information"][0]["status"][ENTITY_MONTHLY_ENERGY] = gwinfo["Total_kwh"] * 0.1 599 | if device_type == str(DEVICE_TYPE_FRIDGE): 600 | self._devices_info[gwid]["Information"][0]["status"][ENTITY_MONTHLY_ENERGY] = gwinfo["Total_kwh"] * 0.1 601 | if device_type == str(DEVICE_TYPE_WASHING_MACHINE): 602 | self._devices_info[gwid]["Information"][0]["status"][ENTITY_MONTHLY_ENERGY] = gwinfo["Total_kwh"] * 0.1 603 | 604 | return True 605 | 606 | @api_status 607 | async def get_update_info(self, check=False): 608 | """ get udpate info 609 | 610 | Returns: 611 | bool: is update info got 612 | """ 613 | 614 | if not check: 615 | for gwid in self._devices_info.keys(): 616 | if "Information" in self._devices_info[gwid]: 617 | self._devices_info[gwid]["Information"][0]["status"][ENTITY_UPDATE] = self._update_info.get(gwid, False) 618 | return False 619 | 620 | for gwid in self._devices_info.keys(): 621 | if len(self._update_info) < 1: 622 | self._update_info[gwid] = False 623 | if "Information" in self._devices_info[gwid]: 624 | self._devices_info[gwid]["Information"][0]["status"][ENTITY_UPDATE] = False 625 | 626 | header = {"CPToken": self._cp_token, "apptype": "Smart"} 627 | response = await self.request( 628 | method="GET", headers=header, endpoint=apis.get_update_info() 629 | ) 630 | 631 | if "GwList" in response: 632 | idx = 0 633 | for gwinfo in response["GwList"]: 634 | gwid = gwinfo.get("GwID", None) 635 | if gwid and "Information" not in self._devices_info[gwid]: 636 | continue 637 | 638 | self._update_info[gwid] = True 639 | self._devices_info[gwid]["Information"][0]["status"][ENTITY_UPDATE] = True 640 | self._devices_info[gwid]["Information"][0]["status"][ENTITY_UPDATE_INFO] = response["UpdateInfo"][idx].get("updateVersion", "") 641 | idx = idx + 1 642 | return True 643 | 644 | @api_status 645 | async def get_plate_info(self, device, check=False): 646 | """ get weight plate info 647 | 648 | Returns: 649 | """ 650 | 651 | if not check: 652 | return 653 | gwid = device["GWID"] 654 | header = { 655 | "CPToken": self._cp_token, 656 | "auth": device["Auth"], 657 | "GWID": gwid 658 | } 659 | response = await self.request( 660 | method="GET", headers=header, endpoint=apis.get_plate_mode() 661 | ) 662 | info = {} 663 | if isinstance(response, dict): 664 | if "State" in response and response["State"] == "success": 665 | info[WEIGHT_PLATE_FOOD_NAME] = response.get("Name", "") 666 | info[WEIGHT_PLATE_MANAGEMENT_MODE] = response.get("ManagementMode", None) 667 | info[WEIGHT_PLATE_MANAGEMENT_VALUE] = response.get("ManagementValue", None) 668 | info[WEIGHT_PLATE_AMOUNT_MAX] = response.get("AmountMax", None) 669 | 670 | dt = response.get("BuyDate", None) 671 | info[WEIGHT_PLATE_BUY_DATE] = datetime.fromtimestamp(int(dt), local_tz) if isinstance(dt, str) else None 672 | dt = response.get("DueDate", None) 673 | info[WEIGHT_PLATE_DUE_DATE] = datetime.fromtimestamp(int(dt), local_tz) if isinstance(dt, str) else None 674 | info[WEIGHT_PLATE_COMMUNICATION_MODE] = response.get("CommunicationMode", None) 675 | info[WEIGHT_PLATE_COMMUNICATION_TIME] = response.get("CommunicationTime", None) 676 | info[WEIGHT_PLATE_TOTAL_WEIGHT] = response.get("TotalWeight", None) 677 | info[WEIGHT_PLATE_RESTORE_WEIGHT] = response.get("RestoreWeight", None) 678 | info[WEIGHT_PLATE_LOW_BATTERY] = response.get("LowBattery", None) 679 | else: 680 | info[WEIGHT_PLATE_FOOD_NAME] = None 681 | info[WEIGHT_PLATE_MANAGEMENT_MODE] = None 682 | info[WEIGHT_PLATE_MANAGEMENT_VALUE] = None 683 | info[WEIGHT_PLATE_AMOUNT_MAX] = None 684 | info[WEIGHT_PLATE_BUY_DATE] = None 685 | info[WEIGHT_PLATE_DUE_DATE] = None 686 | info[WEIGHT_PLATE_COMMUNICATION_MODE] = None 687 | info[WEIGHT_PLATE_COMMUNICATION_TIME] = None 688 | info[WEIGHT_PLATE_TOTAL_WEIGHT] = None 689 | info[WEIGHT_PLATE_RESTORE_WEIGHT] = None 690 | info[WEIGHT_PLATE_LOW_BATTERY] = None 691 | self._devices_info[gwid]["Information"] = [{'DeviceID': 1, 'status': info}] 692 | 693 | @api_status 694 | async def get_devices_with_info(self): 695 | """ 696 | Get devices information 697 | """ 698 | get_update_info = False 699 | if self._api_counts_per_hour < 5: 700 | get_update_info = True 701 | 702 | devices = await self.get_user_devices() 703 | for cmd in self._commands: 704 | self._commands_info[cmd['ModelType']] = cmd["JSON"] 705 | self._refactor_cmds_paras(self._commands_info) 706 | 707 | await asyncio.sleep(.5) 708 | 709 | header = { 710 | "CPToken": self._cp_token, 711 | "apptype": "Smart" 712 | } 713 | response = await self.request( 714 | method="GET", headers=header, endpoint=apis.get_device_status() 715 | ) 716 | 717 | gwid_status = {} 718 | if "GwList" in response: 719 | for dev in response["GwList"]: 720 | gwid = dev["GWID"] 721 | status = "" 722 | for info in dev["List"]: 723 | if info.get("CommandType", "") == "0x00": 724 | status = info["Status"] 725 | break 726 | if info.get("CommandType", "") == "0x50": # Washing Machine 727 | status = info["Status"] 728 | break 729 | if info.get("CommandType", "") == "0x65": # Fridge 730 | status = info["Status"] 731 | break 732 | if info.get("CommandType", "") == "0x63": # JP Fridge 733 | status = info["Status"] 734 | break 735 | if info.get("Status", "") != "": 736 | status = info["Status"] 737 | break 738 | gwid_status[gwid] = status 739 | 740 | for device in devices: 741 | gwid = device["GWID"] 742 | device_type = device["DeviceType"] 743 | model_type = device["ModelType"] 744 | model = device["Model"] 745 | 746 | if len(self._select_devices) >= 1: 747 | if gwid not in self._select_devices: 748 | continue 749 | 750 | if gwid not in self._devices_info: 751 | # _LOGGER.warning(f"gwid not in self._devices_info!") 752 | self._devices_info[gwid] = device 753 | gwid_status[gwid] = "force update" 754 | 755 | if device_type == str(DEVICE_TYPE_WEIGHT_PLATE): 756 | await asyncio.sleep(.1) 757 | await self.get_plate_info(device, get_update_info) 758 | continue 759 | 760 | if device_type == str(DEVICE_TYPE_LIGHT): 761 | gwid_status[gwid] = "force update" 762 | 763 | if len(gwid_status[gwid]) < 1: 764 | # No status code, it maybe offline or power off of washing machine or network busy 765 | # _LOGGER.warning(f"gwid {gwid} is offline {self._devices_info[gwid]}!") 766 | if device_type in [str(DEVICE_TYPE_WASHING_MACHINE)]: 767 | self._devices_info[gwid]["Information"] = self._offline_info(device_type, model_type) 768 | continue 769 | 770 | if not self.is_supported(model_type): 771 | continue 772 | command_types = self._get_commands( 773 | device_type, 774 | model_type, 775 | model 776 | ) 777 | await asyncio.sleep(.1) 778 | await self.get_device_with_info(device, command_types) 779 | await self.get_user_info() 780 | await self.get_update_info(get_update_info) 781 | 782 | return self._devices_info 783 | 784 | @api_status 785 | async def update_device(self, gwid:str, device_id): 786 | """ 787 | Update device status 788 | """ 789 | device = self._devices_info.get(gwid, None) 790 | if not device: 791 | return 792 | 793 | command_types = self._get_commands( 794 | device["DeviceType"], 795 | device["ModelType"], 796 | device["Model"] 797 | ) 798 | await self.get_device_with_info(device, command_types) 799 | 800 | @api_status 801 | async def set_device(self, gwid: str, device_id, func: str, value): 802 | """ 803 | Set device status 804 | """ 805 | auth = "" 806 | 807 | if "Auth" in self._devices_info[gwid]: 808 | auth = self._devices_info[gwid]["Auth"] 809 | if len(auth) <= 1: 810 | _LOGGER.error(f"There is no auth for {gwid}!") 811 | return 812 | device_type = self._devices_info[gwid]["DeviceType"] 813 | cmd = SET_COMMAND_TYPE[device_type].get(func, None) 814 | if cmd is None: 815 | _LOGGER.error(f"There is no cmd for {gwid}!") 816 | cmd = int(func, 16) + 128 817 | 818 | header = {"CPToken": self._cp_token, "auth": auth} 819 | param = {"DeviceID": device_id, "CommandType": cmd, "Value": value} 820 | 821 | await self.request( 822 | method="GET", headers=header, endpoint=apis.set_device(), params=param 823 | ) 824 | 825 | def get_command_name(self, device_gwid:str, command: str) -> str: 826 | """ 827 | Args: 828 | device_gwid (str): the gwid of device 829 | 830 | Returns: 831 | str: the name of command 832 | """ 833 | if device_gwid not in self._devices_info: 834 | return None 835 | 836 | model_type = self._devices_info[device_gwid]["ModelType"] 837 | device_type = self._devices_info[device_gwid]["DeviceType"] 838 | if model_type not in self._commands_info: 839 | return None 840 | cmds_list = self._commands_info[model_type] 841 | for cmds in cmds_list: 842 | if device_type == cmds["DeviceType"]: 843 | cmd_name = cmds.get("CommandName", None) 844 | if cmd_name: 845 | return cmd_name.get(command, None) 846 | 847 | return None 848 | 849 | def get_range(self, device_gwid:str, command: str) -> dict: 850 | """ 851 | Args: 852 | device_gwid (str): the gwid of device 853 | 854 | Returns: 855 | dict: the range dict 856 | """ 857 | rng = {} 858 | if device_gwid not in self._devices_info: 859 | return rng 860 | 861 | model_type = self._devices_info[device_gwid]["ModelType"] 862 | device_type = self._devices_info[device_gwid]["DeviceType"] 863 | if model_type not in self._commands_info: 864 | return rng 865 | cmds_list = self._commands_info[model_type] 866 | for cmds in cmds_list: 867 | if device_type == cmds["DeviceType"]: 868 | cmd_para = cmds.get("CommandParameters", None) 869 | if cmd_para: 870 | rng = cmd_para.get(command, {}) 871 | break 872 | 873 | return rng 874 | 875 | async def async_load_tokens(self) -> dict: 876 | """ 877 | Update tokens in .storage 878 | """ 879 | default = { 880 | CONF_CPTOKEN: "", 881 | CONF_TOKEN_TIMEOUT: "20200101010100", 882 | CONF_REFRESH_TOKEN: "", 883 | CONF_REFRESH_TOKEN_TIMEOUT: "20200101010100" 884 | } 885 | store = Store(self.hass, 1, f"{DOMAIN}/tokens.json") 886 | data = await store.async_load() or None 887 | if not data: 888 | # force login 889 | return default 890 | tokens = data.get(self.email, default) 891 | 892 | # noinspection PyUnusedLocal 893 | async def stop(*args): 894 | # save devices data to .storage 895 | tokens = { 896 | CONF_CPTOKEN: self._cp_token, 897 | CONF_TOKEN_TIMEOUT: self._token_timeout, 898 | CONF_REFRESH_TOKEN: self._refresh_token, 899 | CONF_REFRESH_TOKEN_TIMEOUT: self._refresh_token_timeout 900 | } 901 | data = { 902 | self.email: tokens 903 | } 904 | await store.async_save(data) 905 | 906 | self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop) 907 | return tokens 908 | 909 | async def async_store_tokens(self, tokens: dict): 910 | """ 911 | Update tokens in .storage 912 | """ 913 | store = Store(self.hass, 1, f"{DOMAIN}/tokens.json") 914 | data = await store.async_load() or {} 915 | data = { 916 | self.email: tokens 917 | } 918 | 919 | await store.async_save(data) 920 | 921 | @api_status 922 | async def async_check_tokens(self, tokens=None): 923 | """ 924 | check token is vaild 925 | """ 926 | if tokens is None: 927 | tokens = await self.async_load_tokens() 928 | cptoken = tokens.get(CONF_CPTOKEN, "") 929 | token_timeout = tokens.get( 930 | CONF_TOKEN_TIMEOUT, None) 931 | refresh_token = tokens.get(CONF_REFRESH_TOKEN, "") 932 | refresh_token_timeout = tokens.get( 933 | CONF_REFRESH_TOKEN_TIMEOUT, None) 934 | 935 | if token_timeout is None: 936 | token_timeout = "20200101010100" 937 | if (refresh_token_timeout is None or 938 | isinstance(refresh_token_timeout, str) and len(refresh_token_timeout) < 1): 939 | refresh_token_timeout = "20200101010100" 940 | 941 | now = datetime.now() 942 | updated_refresh_token = False 943 | timeout = datetime( 944 | int(refresh_token_timeout[:4]), 945 | int(refresh_token_timeout[4:6]), 946 | int(refresh_token_timeout[6:8]), 947 | int(refresh_token_timeout[8:10]), 948 | int(refresh_token_timeout[10:12]), 949 | int(refresh_token_timeout[12:]) 950 | ) 951 | 952 | if (int(timeout.timestamp() - now.timestamp()) < 300): 953 | # The maximal API access is 10 per hour 954 | await self.login() 955 | 956 | updated_refresh_token = True 957 | cptoken = self._cp_token 958 | token_timeout = self._token_timeout 959 | refresh_token = self._refresh_token 960 | refresh_token_timeout = self._refresh_token_timeout 961 | await self.async_store_tokens({ 962 | CONF_CPTOKEN: cptoken, 963 | CONF_TOKEN_TIMEOUT: token_timeout, 964 | CONF_REFRESH_TOKEN: refresh_token, 965 | CONF_REFRESH_TOKEN_TIMEOUT: refresh_token_timeout, 966 | }) 967 | self._api_counts_per_hour = 0 968 | 969 | timeout = datetime( 970 | int(token_timeout[:4]), 971 | int(token_timeout[4:6]), 972 | int(token_timeout[6:8]), 973 | int(token_timeout[8:10]), 974 | int(token_timeout[10:12]), 975 | int(token_timeout[12:]) 976 | ) 977 | 978 | if ((int(timeout.timestamp() - now.timestamp()) < 300) and 979 | not updated_refresh_token): 980 | self._refresh_token = refresh_token 981 | await self.refresh_token() 982 | 983 | cptoken = self._cp_token 984 | token_timeout = self._token_timeout 985 | refresh_token = self._refresh_token 986 | refresh_token_timeout = self._refresh_token_timeout 987 | await self.async_store_tokens({ 988 | CONF_CPTOKEN: cptoken, 989 | CONF_TOKEN_TIMEOUT: token_timeout, 990 | CONF_REFRESH_TOKEN: refresh_token, 991 | CONF_REFRESH_TOKEN_TIMEOUT: refresh_token_timeout, 992 | }) 993 | self._api_counts_per_hour = 0 994 | else: 995 | self._cp_token = cptoken 996 | self._token_timeout = token_timeout 997 | self._refresh_token = refresh_token 998 | self._refresh_token_timeout = refresh_token_timeout 999 | 1000 | return { 1001 | CONF_CPTOKEN: cptoken, 1002 | CONF_TOKEN_TIMEOUT: token_timeout, 1003 | CONF_REFRESH_TOKEN: refresh_token, 1004 | CONF_REFRESH_TOKEN_TIMEOUT: refresh_token_timeout, 1005 | } 1006 | 1007 | # @api_status 1008 | async def async_update_data(self): 1009 | """ 1010 | Update data 1011 | """ 1012 | now = datetime.now() 1013 | self._update_timestamp = now.timestamp() 1014 | 1015 | await self.async_check_tokens( 1016 | { 1017 | CONF_CPTOKEN: self._cp_token, 1018 | CONF_TOKEN_TIMEOUT: self._token_timeout, 1019 | CONF_REFRESH_TOKEN: self._refresh_token, 1020 | CONF_REFRESH_TOKEN_TIMEOUT: self._refresh_token_timeout, 1021 | } 1022 | ) 1023 | 1024 | try: 1025 | ret = await self.get_devices_with_info() 1026 | self.hass.data[DOMAIN]["api_counts"] = self._api_counts 1027 | self.hass.data[DOMAIN]["api_counts_per_hour"] = self._api_counts_per_hour 1028 | return ret 1029 | except: 1030 | raise UpdateFailed("Failed while updating device status") 1031 | -------------------------------------------------------------------------------- /custom_components/panasonic_ems2/core/const.py: -------------------------------------------------------------------------------- 1 | """Constants of the Panasonic Smart Home component.""" 2 | 3 | from dataclasses import dataclass 4 | 5 | from homeassistant.components.binary_sensor import ( 6 | BinarySensorDeviceClass, 7 | BinarySensorEntityDescription 8 | ) 9 | 10 | from homeassistant.components.number import ( 11 | NumberEntityDescription 12 | ) 13 | 14 | from homeassistant.components.sensor import ( 15 | SensorDeviceClass, 16 | SensorEntityDescription, 17 | SensorStateClass 18 | ) 19 | 20 | from homeassistant.components.select import ( 21 | SelectEntityDescription 22 | ) 23 | 24 | from homeassistant.components.switch import ( 25 | SwitchDeviceClass, 26 | SwitchEntityDescription 27 | ) 28 | 29 | from homeassistant.components.climate.const import ( 30 | HVACMode, 31 | PRESET_BOOST, 32 | PRESET_ECO, 33 | PRESET_COMFORT, 34 | PRESET_SLEEP, 35 | PRESET_ACTIVITY, 36 | SWING_ON, 37 | SWING_OFF, 38 | SWING_BOTH, 39 | SWING_VERTICAL, 40 | SWING_HORIZONTAL 41 | ) 42 | from homeassistant.const import ( 43 | EntityCategory, 44 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 45 | CONCENTRATION_PARTS_PER_BILLION, 46 | PERCENTAGE, 47 | UnitOfEnergy, 48 | UnitOfMass, 49 | UnitOfTemperature, 50 | UnitOfTime, 51 | UnitOfVolume 52 | ) 53 | 54 | DOMAIN = "panasonic_ems2" 55 | 56 | DOMAINS = [ 57 | "binary_sensor", 58 | "climate", 59 | "fan", 60 | "humidifier", 61 | "number", 62 | "sensor", 63 | "select", 64 | "switch" 65 | ] 66 | 67 | HA_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1" 68 | BASE_URL = 'https://ems2.panasonic.com.tw/api' 69 | APP_TOKEN = "D8CBFF4C-2824-4342-B22D-189166FEF503" 70 | 71 | DATA_CLIENT = "client" 72 | DATA_COORDINATOR = "coordinator" 73 | 74 | CONF_UPDATE_INTERVAL = "update_interval" 75 | CONF_CPTOKEN = "cptoken" 76 | CONF_TOKEN_TIMEOUT = "cptoken_timeout" 77 | CONF_REFRESH_TOKEN = "refresh_token" 78 | CONF_REFRESH_TOKEN_TIMEOUT = "refresh_token_timeout" 79 | 80 | DEFAULT_UPDATE_INTERVAL = 120 81 | REQUEST_TIMEOUT = 15 82 | 83 | DATA_CLIENT = "client" 84 | DATA_COORDINATOR = "coordinator" 85 | UPDATE_LISTENER = "update_listener" 86 | 87 | USER_INFO_TYPES = [ 88 | # "Power", 89 | # "Temp", 90 | # "Humid", 91 | # "PM", 92 | "Other" 93 | ] 94 | 95 | ENTITY_MONTHLY_ENERGY = "0xA0" 96 | ENTITY_DOOR_OPENS = "0xA1" 97 | ENTITY_WATER_USED = "0xA2" 98 | ENTITY_WASH_TIMES = "0xA3" 99 | ENTITY_UPDATE = "0xB0" 100 | ENTITY_UPDATE_INFO = "0xB1" 101 | ENTITY_EMPTY = "0xFF" 102 | 103 | DEVICE_TYPE_CLIMATE = 1 104 | DEVICE_TYPE_FRIDGE = 2 105 | DEVICE_TYPE_WASHING_MACHINE = 3 106 | DEVICE_TYPE_DEHUMIDIFIER = 4 107 | DEVICE_TYPE_AIRPURIFIER = 8 108 | DEVICE_TYPE_ERV = 14 109 | DEVICE_TYPE_FAN = 15 110 | DEVICE_TYPE_LIGHT = 17 111 | DEVICE_TYPE_WEIGHT_PLATE = 23 112 | 113 | AIRPURIFIER_POWER = "0x00" 114 | AIRPURIFIER_OPERATING_MODE = "0x01" 115 | AIRPURIFIER_TIMER_ON = "0x02" 116 | AIRPURIFIER_TIMER_OFF = "0x03" 117 | AIRPURIFIER_AIR_QUALITY = "0x04" 118 | AIRPURIFIER_RESET_FILTER_NOTIFY = "0x05" 119 | AIRPURIFIER_HEAP_REPLACE_NOTIFY = "0x06" 120 | AIRPURIFIER_NANOEX = "0x07" 121 | AIRPURIFIER_LOCK = "0x08" 122 | AIRPURIFIER_ERROR_CODE = "0x09" 123 | AIRPURIFIER_ENERGY = "0x0E" 124 | AIRPURIFIER_PM25 = "0x50" 125 | AIRPURIFIER_51 = "0x51" 126 | AIRPURIFIER_52 = "0x52" 127 | AIRPURIFIER_TIMER_OFF_NEW = "0x53" 128 | AIRPURIFIER_FORMALDEHYDE = "0x54" 129 | AIRPURIFIER_PET_MODE = "0x55" 130 | AIRPURIFIER_LIGHT = "0x56" 131 | AIRPURIFIER_BUZZER = "0x57" 132 | #AIRPURIFIER_LIGHT = "0x62" 133 | AIRPURIFIER_RUNNING_TIME = "0x63" 134 | AIRPURIFIER_RESERVED = "0x7F" 135 | 136 | AIRPURIFIER_NANOEX_PRESET = "nanoe™ X" 137 | AIRPURIFIER_PRESET_MODES = { 138 | AIRPURIFIER_NANOEX: AIRPURIFIER_NANOEX_PRESET, 139 | } 140 | 141 | CLIMATE_AVAILABLE_MODES = { 142 | # HVACMode.OFF: -1, 143 | HVACMode.COOL: 0, 144 | HVACMode.DRY: 1, 145 | HVACMode.FAN_ONLY: 2, 146 | HVACMode.AUTO: 3, 147 | HVACMode.HEAT: 4 148 | } 149 | 150 | CLIMATE_AVAILABLE_SWING_MODES = [ 151 | SWING_ON, 152 | SWING_OFF, 153 | SWING_BOTH, 154 | SWING_VERTICAL, 155 | SWING_HORIZONTAL 156 | ] 157 | CLIMATE_AVAILABLE_FAN_MODES = { 158 | "Auto": 0, 159 | "1": 1, 160 | "2": 2, 161 | "3": 3, 162 | "4": 4, 163 | "5": 5, 164 | "6": 6, 165 | "7": 7, 166 | "8": 8, 167 | "9": 9, 168 | "10": 10, 169 | "11": 11, 170 | "12": 12, 171 | "13": 13, 172 | "14": 14, 173 | "15": 15 174 | } 175 | CLIMATE_MINIMUM_TEMPERATURE = 16 176 | CLIMATE_MAXIMUM_TEMPERATURE = 30 177 | CLIMATE_TEMPERATURE_STEP = 1.0 178 | CLIMATE_ON_TIMER_MIN = 0 179 | CLIMATE_ON_TIMER_MAX = 1440 180 | CLIMATE_OFF_TIMER_MIN = 0 181 | CLIMATE_OFF_TIMER_MAX = 1440 182 | 183 | CLIMATE_POWER = "0x00" 184 | CLIMATE_OPERATING_MODE = "0x01" 185 | CLIMATE_FAN_SPEED = "0x02" 186 | CLIMATE_TARGET_TEMPERATURE = "0x03" 187 | CLIMATE_TEMPERATURE_INDOOR = "0x04" 188 | CLIMATE_SLEEP_MODE = "0x05" 189 | CLIMATE_FUZZY_MODE = "0x07" 190 | CLIMATE_AIRFRESH_MODE = "0x08" 191 | CLIMATE_TIMER_ON = "0x0B" 192 | CLIMATE_TIMER_OFF = "0x0C" 193 | CLIMATE_SWING_VERTICAL = "0x0E" 194 | CLIMATE_SWING_VERTICAL_LEVEL = "0x0F" 195 | CLIMATE_SWING_HORIZONTAL = "0x10" 196 | CLIMATE_SWING_HORIZONTAL_LEVEL = "0x11" 197 | CLIMATE_SET_HUMIDITY = "0x13" 198 | CLIMATE_HUMIDITY_INDOOR = "0x14" 199 | CLIMATE_ERROR_CODE = "0x15" 200 | CLIMATE_ANTI_MILDEW = "0x17" 201 | CLIMATE_AUTO_CLEAN = "0x18" 202 | CLIMATE_ACTIVITY = "0x19" 203 | CLIMATE_BOOST = "0x1A" 204 | CLIMATE_ECO = "0x1B" 205 | CLIMATE_COMFORT = "0x1C" 206 | CLIMATE_BUZZER = "0x1E" 207 | CLIMATE_INDICATOR_LIGHT = "0x1F" 208 | CLIMATE_TEMPERATURE_OUTDOOR = "0x21" 209 | CLIMATE_OPERATING_POWER = "0x27" 210 | CLIMATE_ENERGY = "0x28" 211 | CLIMATE_PM25 = "0x37" 212 | CLIMATE_MONITOR_MILDEW = "0x53" 213 | CLIMATE_61 = "0x61" 214 | CLIMATE_RESERVED = "0x7F" 215 | CLIMATE_PRESET_MODE = "0x80" 216 | CLIMATE_SWING_MODE = "0x81" 217 | 218 | CLIMATE_AVAILABLE_PRESET_MODES = { 219 | CLIMATE_ACTIVITY: PRESET_ACTIVITY, 220 | CLIMATE_BOOST: PRESET_BOOST, 221 | CLIMATE_COMFORT: PRESET_COMFORT, 222 | CLIMATE_ECO: PRESET_ECO, 223 | CLIMATE_SLEEP_MODE: PRESET_SLEEP 224 | } 225 | 226 | CLIMATE_RX_COMMANDS = [ 227 | CLIMATE_ERROR_CODE, 228 | CLIMATE_OPERATING_POWER, 229 | CLIMATE_PM25, 230 | CLIMATE_61 231 | ] 232 | CLIMATE_PXGD_COMMMANDS = [] 233 | CLIMATE_PXGD_MODELS = [ 234 | "J-DUCT", "SX-DUCT", "GX", "LJ", "LX", "PX", "QX", "LJV", "PXGD" "RX-N" 235 | ] 236 | 237 | CLIMATE_PM10_MODELS = [ 238 | "JHW" 239 | ] 240 | 241 | CLIMATE_PM10_2_MODELS = [ 242 | "JHV2" 243 | ] 244 | 245 | CLIMATE_PM25_MODELS = [ 246 | "EHW", "GHW", "JHW", "JHV2" 247 | ] 248 | 249 | DEHUMIDIFIER_POWER = "0x00" 250 | DEHUMIDIFIER_MODE = "0x01" 251 | DEHUMIDIFIER_TIMER_OFF = "0x02" 252 | DEHUMIDIFIER_RELATIVE_HUMIDITY = "0x03" 253 | DEHUMIDIFIER_TARGET_HUMIDITY = "0x04" 254 | DEHUMIDIFIER_HUMIDITY_INDOOR = "0x07" 255 | DEHUMIDIFIER_FAN_SPEED = "0x09" 256 | DEHUMIDIFIER_WATER_TANK_STATUS = "0x0A" 257 | DEHUMIDIFIER_FILTER_CLEAN = "0x0B" 258 | DEHUMIDIFIER_AIRFRESH_MODE = "0x0D" 259 | DEHUMIDIFIER_FAN_MODE = "0x0E" 260 | DEHUMIDIFIER_ERROR_CODE = "0x12" 261 | DEHUMIDIFIER_BUZZER = "0x18" 262 | DEHUMIDIFIER_ENERGY = "0x1D" 263 | DEHUMIDIFIER_50 = "0x50" 264 | DEHUMIDIFIER_51 = "0x51" 265 | DEHUMIDIFIER_PM25 = "0x53" 266 | DEHUMIDIFIER_TIMER_ON = "0x55" 267 | DEHUMIDIFIER_PM10 = "0x56" 268 | DEHUMIDIFIER_58 = "0x58" 269 | DEHUMIDIFIER_59 = "0x59" 270 | 271 | DEHUMIDIFIER_MAX_HUMIDITY = 70 272 | DEHUMIDIFIER_MIN_HUMIDITY = 40 273 | 274 | DEHUMIDIFIER_DEFAULT_MODES = { 275 | "Auto": 0, 276 | "Set": 1, 277 | "Continuous": 2, 278 | "Cloth Dry": 3 279 | } 280 | 281 | DEHUMIDIFIER_PERFORMANCE_MODELS = ["KBS", "LMS", "NM"] 282 | 283 | DEHUMIDIFIER_GHW_COMMANDS = [] 284 | 285 | DEHUMIDIFIER_JHW_COMMANDS = [ 286 | DEHUMIDIFIER_ERROR_CODE, 287 | # DEHUMIDIFIER_51, 288 | DEHUMIDIFIER_PM25, 289 | DEHUMIDIFIER_PM10, 290 | DEHUMIDIFIER_58, 291 | # DEHUMIDIFIER_59 292 | ] 293 | 294 | ERV_POWER = "0x00" 295 | ERV_OPERATING_MODE = "0x01" 296 | ERV_FAN_SPEED = "0x02" 297 | ERV_TARGET_TEMPERATURE = "0x03" 298 | ERV_TEMPERATURE_IN = "0x04" 299 | ERV_TEMPERATURE_OUT = "0x05" 300 | ERV_TIMER_ON = "0x06" 301 | ERV_ERROR_CODE = "0x09" 302 | ERV_ENERGY = "0x0E" 303 | ERV_RESET_FILTER_NOTIFY = "0x14" 304 | ERV_VENTILATE_MODE = "0x15" 305 | ERV_PRE_HEAT_COOL = "0x16" 306 | ERV_REVERED = "0x7F" 307 | 308 | ERV_MINIMUM_TEMPERATURE = -128 309 | ERV_MAXIMUM_TEMPERATURE = 127 310 | 311 | ERV_AVAILABLE_MODES = { 312 | "Cool": 0, 313 | "Dehumidify": 1, 314 | "Fan": 2, 315 | "Auto": 3, 316 | "Heat": 4 317 | } 318 | ERV_AVAILABLE_FAN_MODES = { 319 | "Auto": 0, 320 | "1": 1, 321 | "2": 2, 322 | "3": 3, 323 | "4": 4, 324 | "5": 5, 325 | "6": 6, 326 | "7": 7, 327 | "8": 8, 328 | "9": 9, 329 | "10": 10, 330 | "11": 11, 331 | "12": 12, 332 | "13": 13, 333 | "14": 14, 334 | "15": 15 335 | } 336 | 337 | FAN_POWER = "0x00" 338 | FAN_OPERATING_MODE = "0x01" 339 | FAN_SPEED = "0x02" 340 | FAN_TEMPERATURE_INDOOR = "0x03" 341 | FAN_OSCILLATE = "0x05" 342 | 343 | FAN_PRESET_MODES = { 344 | "mode 1": 0, 345 | "mode 2": 1, 346 | "mode 3": 2, 347 | "mode 4": 3, 348 | "mode 5": 4 349 | } 350 | 351 | FRIDGE_FREEZER_MODE = "0x00" 352 | FRIDGE_CHAMBER_MODE = "0x01" 353 | FRIDGE_FREEZER_TEMPERATURE = "0x03" 354 | FRIDGE_CHAMBER_TEMPERATURE = "0x05" 355 | FRIDGE_ECO = "0x0C" 356 | FRIDGE_ERROR_CODE = "0x0E" 357 | FRIDGE_ENERGY = "0x13" 358 | FRIDGE_DEFROST_SETTING = "0x50" 359 | FRIDGE_STOP_ICE_MAKING = "0x52" 360 | FRIDGE_FAST_ICE_MAKING = "0x53" 361 | FRIDGE_FRESH_QUICK_FREZZE = "0x56" 362 | FRIDGE_THAW_MODE = "0x57" 363 | FRIDGE_THAW_TEMPERATURE = "0x58" 364 | FRIDGE_WINTER_MDOE = "0x5A" 365 | FRIDGE_SHOPPING_MODE = "0x5B" 366 | FRIDGE_GO_OUT_MODE = "0x5C" 367 | FRIDGE_NANOEX = "0x61" 368 | FRIDGE_ERROR_CODE_JP = "0x63" 369 | 370 | FRIDGE_XGS_COMMANDS = [ 371 | FRIDGE_ECO, 372 | FRIDGE_FREEZER_TEMPERATURE, 373 | FRIDGE_CHAMBER_TEMPERATURE, 374 | FRIDGE_THAW_TEMPERATURE, 375 | FRIDGE_ENERGY, 376 | FRIDGE_NANOEX 377 | ] 378 | 379 | FRIDGE_MODELS = [ 380 | "NR-F655WX-X1", "NR-F655WX-X", "NR-F655WPX" 381 | ] 382 | 383 | FRIDGE_2020_MODELS = [ 384 | "NR-F506HX-N1", "NR-F506HX-W1", "NR-F506HX-X1", "NR-F556HX-N1", 385 | "NR-F556HX-W1", "NR-F556HX-X1", "NR-F606HX-N1", "NR-F606HX-W1", 386 | "NR-F606HX-X1", "NR-F656WX-X1" 387 | ] 388 | 389 | LIGHT_POWER = "0x00" 390 | LIGHT_PERCENTAGE = "0x01" 391 | LIGHT_OPERATION_STATE = "0x70" 392 | LIGHT_CHANNEL_1_TIMER_ON = "0x71" 393 | LIGHT_CHANNEL_1_TIMER_OFF = "0x72" 394 | LIGHT_MAINTAIN_MODE = "0x73" 395 | LIGHT_CHANNEL_2_TIMER_ON = "0x74" 396 | LIGHT_CHANNEL_2_TIMER_OFF = "0x75" 397 | LIGHT_CHANNEL_3_TIMER_ON = "0x76" 398 | LIGHT_CHANNEL_3_TIMER_OFF = "0x77" 399 | LIGHT_RESERVED = "0x7F" 400 | 401 | LIGHT_WTY_COMMANDS = [ 402 | # LIGHT_OPERATION_STATE, 403 | # LIGHT_CHANNEL_1_TIMER_ON, 404 | # LIGHT_CHANNEL_1_TIMER_OFF, 405 | LIGHT_MAINTAIN_MODE, 406 | # LIGHT_CHANNEL_2_TIMER_ON, 407 | # LIGHT_CHANNEL_2_TIMER_OFF, 408 | # LIGHT_CHANNEL_3_TIMER_ON, 409 | # LIGHT_CHANNEL_3_TIMER_OFF 410 | ] 411 | 412 | WASHING_MACHINE_POWER = "0x00" 413 | WASHING_MACHINE_ENABLE = "0x01" 414 | WASHING_MACHINE_PROGRESS = "0x02" 415 | WASHING_MACHINE_OPERATING_STATUS_OLD = "0x03" 416 | WASHING_MACHINE_REMAING_WASH_TIME= "0x13" 417 | WASHING_MACHINE_TIMER = "0x14" 418 | WASHING_MACHINE_TIMER_REMAINING_TIME_OLD = "0x15" 419 | WASHING_MACHINE_ERROR_CODE = "0x19" 420 | WASHING_MACHINE_ENERGY = "0x1E" 421 | WASHING_MACHINE_OPERATING_STATUS = "0x50" 422 | WASHING_MACHINE_51 = "0x51" 423 | WASHING_MACHINE_52 = "0x52" 424 | WASHING_MACHINE_53 = "0x53" 425 | WASHING_MACHINE_CURRENT_MODE = "0x54" 426 | WASHING_MACHINE_CURRENT_PROGRESS = "0x55" 427 | WASHING_MACHINE_POSTPONE_DRYING = "0x56" 428 | WASHING_MACHINE_57 = "0x57" 429 | WASHING_MACHINE_TIMER_REMAINING_TIME = "0x58" 430 | WASHING_MACHINE_59 = "0x59" 431 | WASHING_MACHINE_60 = "0x60" 432 | WASHING_MACHINE_61 = "0x61" 433 | WASHING_MACHINE_PROGRESS_NEW = "0x64" 434 | WASHING_MACHINE_66 = "0x66" 435 | WASHING_MACHINE_67 = "0x67" 436 | WASHING_MACHINE_68 = "0x68" 437 | WASHING_MACHINE_WARM_WATER = "0x69" 438 | WASHING_MACHINE_71 = "0x71" 439 | WASHING_MACHINE_72 = "0x72" 440 | WASHING_MACHINE_73 = "0x73" 441 | WASHING_MACHINE_REMOTE_CONTROL = "0x74" 442 | 443 | WASHING_MACHINE_MODELS = ["DDH", "DW","HDH", "MDH"] 444 | WASHING_MACHINE_2020_MODELS = ["KBS", "LM", "LMS"] 445 | 446 | WASHING_MACHINE_LX128B_COMMANDS = [ 447 | WASHING_MACHINE_71, 448 | WASHING_MACHINE_72, 449 | WASHING_MACHINE_73 450 | ] 451 | 452 | WASHING_MACHINE_HDH_COMMANDS = [ 453 | WASHING_MACHINE_OPERATING_STATUS_OLD, 454 | WASHING_MACHINE_TIMER_REMAINING_TIME_OLD, 455 | WASHING_MACHINE_53, 456 | WASHING_MACHINE_57, 457 | # WASHING_MACHINE_68 458 | ] 459 | 460 | WASHING_MACHINE_KBS_COMMANDS = [ 461 | WASHING_MACHINE_TIMER_REMAINING_TIME_OLD 462 | ] 463 | 464 | WEIGHT_PLATE_GET_WEIGHT = "0x52" 465 | WEIGHT_PLATE_FOOD_NAME = "0x80" 466 | WEIGHT_PLATE_MANAGEMENT_MODE = "0x81" 467 | WEIGHT_PLATE_MANAGEMENT_VALUE = "0x82" 468 | WEIGHT_PLATE_AMOUNT_MAX = "0x83" 469 | WEIGHT_PLATE_BUY_DATE = "0x84" 470 | WEIGHT_PLATE_DUE_DATE = "0x85" 471 | WEIGHT_PLATE_COMMUNICATION_MODE = "0x8A" 472 | WEIGHT_PLATE_COMMUNICATION_TIME = "0x8B" 473 | WEIGHT_PLATE_TOTAL_WEIGHT = "0x8C" 474 | WEIGHT_PLATE_RESTORE_WEIGHT = "0x8D" 475 | WEIGHT_PLATE_LOW_BATTERY = "0x8E" 476 | 477 | MODEL_JP_TYPES = [ 478 | "F655", 479 | "F656", 480 | "F657", 481 | "F658", 482 | "F659", 483 | "LX128B" 484 | ] 485 | 486 | COMMANDS_TYPE= { 487 | str(DEVICE_TYPE_AIRPURIFIER): [ 488 | AIRPURIFIER_POWER, 489 | AIRPURIFIER_OPERATING_MODE, 490 | #AIRPURIFIER_TIMER_ON, 491 | #AIRPURIFIER_TIMER_OFF, 492 | #AIRPURIFIER_AIR_QUALITY, 493 | AIRPURIFIER_ENERGY, 494 | AIRPURIFIER_HEAP_REPLACE_NOTIFY, 495 | AIRPURIFIER_NANOEX, 496 | AIRPURIFIER_PET_MODE, 497 | AIRPURIFIER_BUZZER, 498 | AIRPURIFIER_PM25, 499 | AIRPURIFIER_LIGHT, 500 | AIRPURIFIER_51, 501 | AIRPURIFIER_52, 502 | AIRPURIFIER_TIMER_OFF_NEW, 503 | AIRPURIFIER_FORMALDEHYDE 504 | ], 505 | str(DEVICE_TYPE_CLIMATE): [ 506 | CLIMATE_POWER, 507 | CLIMATE_OPERATING_MODE, 508 | CLIMATE_FAN_SPEED, 509 | CLIMATE_TARGET_TEMPERATURE, 510 | CLIMATE_TEMPERATURE_INDOOR, 511 | CLIMATE_SLEEP_MODE, 512 | CLIMATE_AIRFRESH_MODE, 513 | CLIMATE_TIMER_ON, 514 | CLIMATE_TIMER_OFF, 515 | CLIMATE_SWING_VERTICAL_LEVEL, 516 | CLIMATE_SWING_HORIZONTAL_LEVEL, 517 | CLIMATE_ANTI_MILDEW, 518 | CLIMATE_AUTO_CLEAN, 519 | CLIMATE_ACTIVITY, 520 | CLIMATE_BOOST, 521 | CLIMATE_ECO, 522 | CLIMATE_BUZZER, 523 | CLIMATE_INDICATOR_LIGHT, 524 | CLIMATE_ENERGY, 525 | CLIMATE_TEMPERATURE_OUTDOOR 526 | ], 527 | str(DEVICE_TYPE_DEHUMIDIFIER): [ 528 | DEHUMIDIFIER_POWER, 529 | DEHUMIDIFIER_MODE, 530 | DEHUMIDIFIER_TIMER_OFF, 531 | DEHUMIDIFIER_TARGET_HUMIDITY, 532 | DEHUMIDIFIER_HUMIDITY_INDOOR, 533 | DEHUMIDIFIER_FAN_SPEED, 534 | DEHUMIDIFIER_WATER_TANK_STATUS, 535 | DEHUMIDIFIER_AIRFRESH_MODE, 536 | DEHUMIDIFIER_FAN_MODE, 537 | DEHUMIDIFIER_BUZZER, 538 | DEHUMIDIFIER_ENERGY, 539 | DEHUMIDIFIER_50, 540 | DEHUMIDIFIER_TIMER_ON 541 | ], 542 | str(DEVICE_TYPE_ERV): [ 543 | ERV_POWER, 544 | ERV_OPERATING_MODE, 545 | ERV_FAN_SPEED, 546 | ERV_TARGET_TEMPERATURE, 547 | ERV_TEMPERATURE_IN, 548 | ERV_TEMPERATURE_OUT, 549 | ERV_ERROR_CODE, 550 | ERV_ENERGY 551 | ], 552 | str(DEVICE_TYPE_FRIDGE): [ 553 | FRIDGE_FREEZER_MODE, 554 | FRIDGE_CHAMBER_MODE, 555 | FRIDGE_DEFROST_SETTING, 556 | FRIDGE_STOP_ICE_MAKING, 557 | FRIDGE_FAST_ICE_MAKING, 558 | FRIDGE_FRESH_QUICK_FREZZE, 559 | FRIDGE_THAW_MODE, 560 | FRIDGE_WINTER_MDOE, 561 | FRIDGE_SHOPPING_MODE, 562 | FRIDGE_GO_OUT_MODE 563 | ], 564 | str(DEVICE_TYPE_LIGHT): [ 565 | LIGHT_POWER 566 | ], 567 | str(DEVICE_TYPE_WASHING_MACHINE): [ 568 | WASHING_MACHINE_ENABLE, 569 | WASHING_MACHINE_REMAING_WASH_TIME, 570 | WASHING_MACHINE_TIMER, 571 | WASHING_MACHINE_ERROR_CODE, 572 | WASHING_MACHINE_TIMER_REMAINING_TIME, 573 | WASHING_MACHINE_ENERGY, 574 | WASHING_MACHINE_OPERATING_STATUS, 575 | WASHING_MACHINE_CURRENT_MODE, 576 | WASHING_MACHINE_CURRENT_PROGRESS, 577 | WASHING_MACHINE_POSTPONE_DRYING, 578 | WASHING_MACHINE_PROGRESS, 579 | WASHING_MACHINE_WARM_WATER, 580 | WASHING_MACHINE_52, 581 | WASHING_MACHINE_66, 582 | WASHING_MACHINE_67, 583 | WASHING_MACHINE_REMOTE_CONTROL 584 | ], 585 | str(DEVICE_TYPE_WEIGHT_PLATE): [ 586 | WEIGHT_PLATE_GET_WEIGHT 587 | ] 588 | } 589 | 590 | EXTRA_COMMANDS = { 591 | str(DEVICE_TYPE_CLIMATE): { 592 | # "RX-N": CLIMATE_RX_COMMANDS + [CLIMATE_MONITOR_MILDEW], 593 | "RX-N": CLIMATE_RX_COMMANDS, 594 | "RX-G": CLIMATE_RX_COMMANDS, 595 | "RX-J": CLIMATE_RX_COMMANDS 596 | }, 597 | str(DEVICE_TYPE_DEHUMIDIFIER): { 598 | "JHW": DEHUMIDIFIER_JHW_COMMANDS 599 | }, 600 | str(DEVICE_TYPE_ERV): { 601 | }, 602 | str(DEVICE_TYPE_FRIDGE): { 603 | "XGS": FRIDGE_XGS_COMMANDS, 604 | "F655": [FRIDGE_ERROR_CODE_JP], 605 | "F656": [FRIDGE_ERROR_CODE_JP], 606 | "F657": [FRIDGE_ERROR_CODE_JP], 607 | "F658": [FRIDGE_ERROR_CODE_JP], 608 | "F659": [FRIDGE_ERROR_CODE_JP] 609 | }, 610 | str(DEVICE_TYPE_LIGHT): { 611 | "WTY": LIGHT_WTY_COMMANDS 612 | }, 613 | str(DEVICE_TYPE_WASHING_MACHINE): { 614 | "LX128B": WASHING_MACHINE_LX128B_COMMANDS, 615 | "DDH": WASHING_MACHINE_HDH_COMMANDS, 616 | "DW": WASHING_MACHINE_HDH_COMMANDS, 617 | "HDH": WASHING_MACHINE_HDH_COMMANDS, 618 | "MDH": WASHING_MACHINE_HDH_COMMANDS, 619 | "KBS": WASHING_MACHINE_KBS_COMMANDS, 620 | "LM": WASHING_MACHINE_KBS_COMMANDS, 621 | "LMS": WASHING_MACHINE_KBS_COMMANDS 622 | }, 623 | str(DEVICE_TYPE_AIRPURIFIER): { 624 | } 625 | } 626 | 627 | EXCESS_COMMANDS = { 628 | str(DEVICE_TYPE_CLIMATE): { 629 | "J-DUCT": [CLIMATE_SWING_VERTICAL_LEVEL, CLIMATE_SWING_HORIZONTAL_LEVEL], 630 | }, 631 | str(DEVICE_TYPE_DEHUMIDIFIER): { 632 | }, 633 | str(DEVICE_TYPE_ERV): { 634 | }, 635 | str(DEVICE_TYPE_FRIDGE): { 636 | }, 637 | str(DEVICE_TYPE_LIGHT): { 638 | }, 639 | str(DEVICE_TYPE_WASHING_MACHINE): { 640 | }, 641 | str(DEVICE_TYPE_AIRPURIFIER): { 642 | } 643 | } 644 | 645 | SET_COMMAND_TYPE = { 646 | str(DEVICE_TYPE_AIRPURIFIER): { 647 | AIRPURIFIER_POWER: 0, 648 | AIRPURIFIER_OPERATING_MODE: 1, 649 | AIRPURIFIER_NANOEX: 135, 650 | AIRPURIFIER_PET_MODE: 85, 651 | AIRPURIFIER_LIGHT: 86, 652 | AIRPURIFIER_BUZZER: 87 653 | }, 654 | str(DEVICE_TYPE_CLIMATE): { 655 | CLIMATE_PRESET_MODE: 1, 656 | CLIMATE_TARGET_TEMPERATURE: 3, 657 | CLIMATE_SLEEP_MODE: 5, 658 | CLIMATE_ANTI_MILDEW: 23, 659 | CLIMATE_AUTO_CLEAN: 24, 660 | CLIMATE_BUZZER: 30, 661 | CLIMATE_POWER: 128, 662 | CLIMATE_OPERATING_MODE: 129, 663 | CLIMATE_FAN_SPEED: 130, 664 | CLIMATE_ECO: 136, 665 | CLIMATE_TIMER_ON: 139, 666 | CLIMATE_TIMER_OFF: 140, 667 | CLIMATE_SWING_MODE: 143, 668 | CLIMATE_ACTIVITY: 153, 669 | CLIMATE_BOOST: 154, 670 | CLIMATE_AUTO_CLEAN: 155, 671 | CLIMATE_INDICATOR_LIGHT: 159 672 | }, 673 | str(DEVICE_TYPE_DEHUMIDIFIER): { 674 | DEHUMIDIFIER_POWER: 128, 675 | DEHUMIDIFIER_MODE: 129, 676 | DEHUMIDIFIER_TARGET_HUMIDITY: 132, 677 | DEHUMIDIFIER_FAN_SPEED: 137, 678 | DEHUMIDIFIER_AIRFRESH_MODE: 141, 679 | DEHUMIDIFIER_FAN_MODE: 142, 680 | DEHUMIDIFIER_BUZZER: 152, 681 | DEHUMIDIFIER_TIMER_ON: 213 682 | }, 683 | str(DEVICE_TYPE_ERV): { 684 | ERV_POWER: 0 685 | }, 686 | str(DEVICE_TYPE_LIGHT): { 687 | LIGHT_POWER: 0, 688 | LIGHT_OPERATION_STATE: 112, 689 | LIGHT_CHANNEL_1_TIMER_ON: 113, 690 | LIGHT_CHANNEL_1_TIMER_OFF: 114, 691 | LIGHT_MAINTAIN_MODE: 115, 692 | LIGHT_CHANNEL_2_TIMER_ON: 116, 693 | LIGHT_CHANNEL_2_TIMER_OFF: 117, 694 | LIGHT_CHANNEL_3_TIMER_ON: 118, 695 | LIGHT_CHANNEL_3_TIMER_OFF: 119 696 | }, 697 | str(DEVICE_TYPE_WASHING_MACHINE): { 698 | WASHING_MACHINE_ENABLE: 1, 699 | WASHING_MACHINE_TIMER: 20, 700 | WASHING_MACHINE_PROGRESS: 130, 701 | WASHING_MACHINE_PROGRESS_NEW: 100, 702 | WASHING_MACHINE_WARM_WATER: 105 703 | }, 704 | str(DEVICE_TYPE_FRIDGE): { 705 | FRIDGE_FREEZER_MODE: 0, 706 | FRIDGE_CHAMBER_MODE: 1, 707 | FRIDGE_FREEZER_TEMPERATURE: 3, 708 | FRIDGE_CHAMBER_TEMPERATURE: 5, 709 | FRIDGE_ECO: 12, 710 | FRIDGE_ERROR_CODE: 14, 711 | FRIDGE_ENERGY: 19, 712 | FRIDGE_DEFROST_SETTING: 80, 713 | FRIDGE_STOP_ICE_MAKING: 82, 714 | FRIDGE_FAST_ICE_MAKING: 83, 715 | FRIDGE_FRESH_QUICK_FREZZE: 86, 716 | FRIDGE_THAW_MODE: 87, 717 | FRIDGE_THAW_TEMPERATURE: 88, 718 | FRIDGE_WINTER_MDOE: 90, 719 | FRIDGE_SHOPPING_MODE: 91, 720 | FRIDGE_GO_OUT_MODE: 92, 721 | FRIDGE_NANOEX: 97, 722 | FRIDGE_ERROR_CODE_JP: 99 723 | } 724 | } 725 | 726 | 727 | @dataclass 728 | class PanasonicBinarySensorDescription( 729 | BinarySensorEntityDescription 730 | ): 731 | """Class to describe an Panasonic binary sensor.""" 732 | options_value: list[str] | None = None 733 | 734 | 735 | AIRPURIFIER_BINARY_SENSORS: tuple[PanasonicBinarySensorDescription, ...] = ( 736 | PanasonicBinarySensorDescription( 737 | key=ENTITY_UPDATE, 738 | name="Firmware Update", 739 | icon='mdi:package-up', 740 | device_class=BinarySensorDeviceClass.UPDATE 741 | ), 742 | PanasonicBinarySensorDescription( 743 | key=AIRPURIFIER_HEAP_REPLACE_NOTIFY, 744 | name="HEAP Filter Replace", 745 | icon='mdi:filter-variant-remove' 746 | ) 747 | ) 748 | 749 | CLIMATE_BINARY_SENSORS: tuple[PanasonicBinarySensorDescription, ...] = ( 750 | PanasonicBinarySensorDescription( 751 | key=ENTITY_UPDATE, 752 | name="Firmware Update", 753 | icon='mdi:package-up', 754 | device_class=BinarySensorDeviceClass.UPDATE 755 | ), 756 | PanasonicBinarySensorDescription( 757 | key=ENTITY_EMPTY, 758 | name="Empty", 759 | icon='mdi:cog' 760 | ) 761 | ) 762 | 763 | DEHUMIDIFIER_BINARY_SENSORS: tuple[PanasonicBinarySensorDescription, ...] = ( 764 | PanasonicBinarySensorDescription( 765 | key=ENTITY_UPDATE, 766 | name="Firmware Update", 767 | icon='mdi:package-up', 768 | device_class=BinarySensorDeviceClass.UPDATE 769 | ), 770 | PanasonicBinarySensorDescription( 771 | key=DEHUMIDIFIER_WATER_TANK_STATUS, 772 | name="Water Tank", 773 | icon='mdi:cup-water' 774 | ) 775 | ) 776 | 777 | ERV_BINARY_SENSORS: tuple[PanasonicBinarySensorDescription, ...] = ( 778 | PanasonicBinarySensorDescription( 779 | key=ENTITY_UPDATE, 780 | name="Firmware Update", 781 | icon='mdi:package-up', 782 | device_class=BinarySensorDeviceClass.UPDATE 783 | ), 784 | PanasonicBinarySensorDescription( 785 | key=ENTITY_EMPTY, 786 | name="Empty", 787 | icon='mdi:cog' 788 | ) 789 | ) 790 | 791 | FRIDGE_BINARY_SENSORS: tuple[PanasonicBinarySensorDescription, ...] = ( 792 | PanasonicBinarySensorDescription( 793 | key=ENTITY_UPDATE, 794 | name="Firmware Update", 795 | icon='mdi:package-up', 796 | device_class=BinarySensorDeviceClass.UPDATE 797 | ), 798 | PanasonicBinarySensorDescription( 799 | key=ENTITY_EMPTY, 800 | name="Empty", 801 | icon='mdi:cog' 802 | ) 803 | ) 804 | 805 | LIGHT_BINARY_SENSORS: tuple[PanasonicBinarySensorDescription, ...] = ( 806 | PanasonicBinarySensorDescription( 807 | key=ENTITY_UPDATE, 808 | name="Firmware Update", 809 | icon='mdi:package-up', 810 | device_class=BinarySensorDeviceClass.UPDATE 811 | ), 812 | PanasonicBinarySensorDescription( 813 | key=ENTITY_EMPTY, 814 | name="Empty", 815 | icon='mdi:cog' 816 | ) 817 | ) 818 | 819 | WASHING_MACHINE_BINARY_SENSORS: tuple[PanasonicBinarySensorDescription, ...] = ( 820 | PanasonicBinarySensorDescription( 821 | key=ENTITY_UPDATE, 822 | name="Firmware Update", 823 | icon='mdi:package-up', 824 | device_class=BinarySensorDeviceClass.UPDATE 825 | ), 826 | PanasonicBinarySensorDescription( 827 | key=ENTITY_EMPTY, 828 | name="Empty", 829 | icon='mdi:cog' 830 | ) 831 | ) 832 | 833 | @dataclass 834 | class PanasonicNumberDescription( 835 | NumberEntityDescription 836 | ): 837 | """Class to describe an Panasonic number.""" 838 | options_value: list[str] | None = None 839 | 840 | 841 | AIRPURIFIER_NUMBERS: tuple[PanasonicNumberDescription, ...] = ( 842 | PanasonicNumberDescription( 843 | key=AIRPURIFIER_TIMER_ON, 844 | name="Timer On", 845 | native_unit_of_measurement=UnitOfTime.HOURS, 846 | entity_category=EntityCategory.CONFIG, 847 | icon='mdi:timer-cog-outline', 848 | native_min_value=0, 849 | native_max_value=24, 850 | native_step=1, 851 | entity_registry_enabled_default=False 852 | ), 853 | PanasonicNumberDescription( 854 | key=AIRPURIFIER_TIMER_OFF, 855 | name="Timer Off", 856 | native_unit_of_measurement=UnitOfTime.HOURS, 857 | entity_category=EntityCategory.CONFIG, 858 | icon='mdi:timer-cog', 859 | native_min_value=0, 860 | native_max_value=24, 861 | native_step=1, 862 | entity_registry_enabled_default=False 863 | ) 864 | ) 865 | 866 | CLIMATE_NUMBERS: tuple[PanasonicNumberDescription, ...] = ( 867 | PanasonicNumberDescription( 868 | key=CLIMATE_TIMER_ON, 869 | name="Timer On", 870 | native_unit_of_measurement=UnitOfTime.MINUTES, 871 | entity_category=EntityCategory.CONFIG, 872 | icon='mdi:timer-cog-outline', 873 | native_min_value=0, 874 | native_max_value=1440, 875 | native_step=1, 876 | entity_registry_enabled_default=False 877 | ), 878 | PanasonicNumberDescription( 879 | key=CLIMATE_TIMER_OFF, 880 | name="Timer Off", 881 | native_unit_of_measurement=UnitOfTime.MINUTES, 882 | entity_category=EntityCategory.CONFIG, 883 | icon='mdi:timer-cog', 884 | native_min_value=0, 885 | native_max_value=1440, 886 | native_step=1, 887 | entity_registry_enabled_default=False 888 | ) 889 | ) 890 | 891 | DEHUMIDIFIER_NUMBERS: tuple[PanasonicNumberDescription, ...] = ( 892 | PanasonicNumberDescription( 893 | key=DEHUMIDIFIER_TIMER_ON, 894 | name="Timer On", 895 | native_unit_of_measurement=UnitOfTime.HOURS, 896 | entity_category=EntityCategory.CONFIG, 897 | icon='mdi:timer-cog-outline', 898 | native_min_value=0, 899 | native_max_value=12, 900 | native_step=1, 901 | entity_registry_enabled_default=False 902 | ), 903 | PanasonicNumberDescription( 904 | key=DEHUMIDIFIER_TIMER_OFF, 905 | name="Timer Off", 906 | native_unit_of_measurement=UnitOfTime.HOURS, 907 | entity_category=EntityCategory.CONFIG, 908 | icon='mdi:timer-cog', 909 | native_min_value=0, 910 | native_max_value=12, 911 | native_step=1, 912 | entity_registry_enabled_default=False 913 | ) 914 | ) 915 | 916 | ERV_NUMBERS: tuple[PanasonicNumberDescription, ...] = ( 917 | PanasonicNumberDescription( 918 | key=ERV_TARGET_TEMPERATURE, 919 | name="Target Temperature", 920 | native_unit_of_measurement=UnitOfTemperature.CELSIUS, 921 | entity_category=EntityCategory.CONFIG, 922 | icon='mdi:thermometer', 923 | native_min_value=-128, 924 | native_max_value=127, 925 | native_step=1, 926 | entity_registry_enabled_default=False 927 | ), 928 | PanasonicNumberDescription( 929 | key=ERV_TIMER_ON, 930 | name="Timer On", 931 | native_unit_of_measurement=UnitOfTime.MINUTES, 932 | entity_category=EntityCategory.CONFIG, 933 | icon='mdi:timer-cog', 934 | native_min_value=0, 935 | native_max_value=1440, 936 | native_step=1, 937 | entity_registry_enabled_default=False 938 | ) 939 | ) 940 | 941 | LIGHT_NUMBERS: tuple[PanasonicNumberDescription, ...] = ( 942 | PanasonicNumberDescription( 943 | key=LIGHT_CHANNEL_1_TIMER_ON, 944 | name="Channel 1 Timer On", 945 | native_unit_of_measurement=UnitOfTime.HOURS, 946 | entity_category=EntityCategory.CONFIG, 947 | icon='mdi:timer-cog-outline', 948 | native_min_value=0, 949 | native_max_value=24, 950 | native_step=1, 951 | entity_registry_enabled_default=False 952 | ), 953 | PanasonicNumberDescription( 954 | key=LIGHT_CHANNEL_1_TIMER_OFF, 955 | name="Channel 1 Timer Off", 956 | native_unit_of_measurement=UnitOfTime.HOURS, 957 | entity_category=EntityCategory.CONFIG, 958 | icon='mdi:timer-cog', 959 | native_min_value=0, 960 | native_max_value=24, 961 | native_step=1, 962 | entity_registry_enabled_default=False 963 | ), 964 | PanasonicNumberDescription( 965 | key=LIGHT_CHANNEL_2_TIMER_ON, 966 | name="Channel 2 Timer On", 967 | native_unit_of_measurement=UnitOfTime.HOURS, 968 | entity_category=EntityCategory.CONFIG, 969 | icon='mdi:timer-cog-outline', 970 | native_min_value=0, 971 | native_max_value=24, 972 | native_step=1, 973 | entity_registry_enabled_default=False 974 | ), 975 | PanasonicNumberDescription( 976 | key=LIGHT_CHANNEL_2_TIMER_OFF, 977 | name="Channel 2 Timer Off", 978 | native_unit_of_measurement=UnitOfTime.HOURS, 979 | entity_category=EntityCategory.CONFIG, 980 | icon='mdi:timer-cog', 981 | native_min_value=0, 982 | native_max_value=24, 983 | native_step=1, 984 | entity_registry_enabled_default=False 985 | ), 986 | PanasonicNumberDescription( 987 | key=LIGHT_CHANNEL_3_TIMER_ON, 988 | name="Channel 3 Timer On", 989 | native_unit_of_measurement=UnitOfTime.HOURS, 990 | entity_category=EntityCategory.CONFIG, 991 | icon='mdi:timer-cog-outline', 992 | native_min_value=0, 993 | native_max_value=24, 994 | native_step=1, 995 | entity_registry_enabled_default=False 996 | ), 997 | PanasonicNumberDescription( 998 | key=LIGHT_CHANNEL_3_TIMER_OFF, 999 | name="Channel 3 Timer Off", 1000 | native_unit_of_measurement=UnitOfTime.HOURS, 1001 | entity_category=EntityCategory.CONFIG, 1002 | icon='mdi:timer-cog', 1003 | native_min_value=0, 1004 | native_max_value=24, 1005 | native_step=1, 1006 | entity_registry_enabled_default=False 1007 | ) 1008 | ) 1009 | 1010 | @dataclass 1011 | class PanasonicSelectDescription( 1012 | SelectEntityDescription 1013 | ): 1014 | """Class to describe an Panasonic select.""" 1015 | options_value: list[str] | None = None 1016 | 1017 | 1018 | AIRPURIFIER_SELECTS: tuple[PanasonicSelectDescription, ...] = ( 1019 | PanasonicSelectDescription( 1020 | key=AIRPURIFIER_LIGHT, 1021 | name="Light", 1022 | entity_category=EntityCategory.CONFIG, 1023 | icon='mdi:brightness-5', 1024 | options=["Light", "Dark", "Off"], 1025 | options_value=["0", "1", "2"] 1026 | ), 1027 | PanasonicSelectDescription( 1028 | key=AIRPURIFIER_OPERATING_MODE, 1029 | name="Fan Mode", 1030 | entity_category=EntityCategory.CONFIG, 1031 | icon='mdi:fan', 1032 | options=["Auto", "Mute", "Week", "Middle", "Strong"], 1033 | options_value=["0", "1", "2", "3", "4"] 1034 | ), 1035 | PanasonicSelectDescription( 1036 | key=AIRPURIFIER_RESERVED, 1037 | name="Reserved", 1038 | entity_category=EntityCategory.CONFIG, 1039 | icon='mdi:help', 1040 | options=[], 1041 | options_value=[] 1042 | ) 1043 | ) 1044 | 1045 | CLIMATE_SELECTS: tuple[PanasonicSelectDescription, ...] = ( 1046 | PanasonicSelectDescription( 1047 | key=CLIMATE_FUZZY_MODE, 1048 | name="Fuzzy Mode", 1049 | entity_category=EntityCategory.CONFIG, 1050 | icon='mdi:home-thermometer-outline', 1051 | options=["Better", "Too cloud", "Too hot", "Off", "On"], 1052 | options_value=["0", "1", "2", "3", "4"], 1053 | ), 1054 | PanasonicSelectDescription( 1055 | key=CLIMATE_ACTIVITY, 1056 | name="Motion Detect", 1057 | entity_category=EntityCategory.CONFIG, 1058 | icon='mdi:motion-sensor', 1059 | options=["Off", "To human", "Not to human", "Auto"], 1060 | options_value=["0", "1", "2", "3"] 1061 | ), 1062 | PanasonicSelectDescription( 1063 | key=CLIMATE_INDICATOR_LIGHT, 1064 | name="Indicator Light", 1065 | entity_category=EntityCategory.CONFIG, 1066 | icon='mdi:lightbulb', 1067 | options=["Light", "Dark", "Off"], 1068 | options_value=["0", "1", "2"] 1069 | ), 1070 | PanasonicSelectDescription( 1071 | key=CLIMATE_SWING_VERTICAL_LEVEL, 1072 | name="Vertical Fan Level", 1073 | entity_category=EntityCategory.CONFIG, 1074 | icon='mdi:fan', 1075 | options=["Auto", "1", "2", "3", "4"], 1076 | options_value=["0", "1", "2", "3", "4"], 1077 | ), 1078 | PanasonicSelectDescription( 1079 | key=CLIMATE_SWING_HORIZONTAL_LEVEL, 1080 | name="Horizontal Fan Level", 1081 | entity_category=EntityCategory.CONFIG, 1082 | icon='mdi:fan', 1083 | options=["Auto", "1", "2", "3", "4"], 1084 | options_value=["0", "1", "2", "3", "4"], 1085 | ), 1086 | ) 1087 | 1088 | DEHUMIDIFIER_SELECTS: tuple[PanasonicSelectDescription, ...] = ( 1089 | PanasonicSelectDescription( 1090 | key=DEHUMIDIFIER_FAN_SPEED, 1091 | name="Fan Speed", 1092 | entity_category=EntityCategory.CONFIG, 1093 | icon='mdi:fan', 1094 | options=["Auto", "Slience", "Standard", "Speed"], 1095 | options_value=["0", "1", "2", "3"], 1096 | ), 1097 | PanasonicSelectDescription( 1098 | key=DEHUMIDIFIER_FAN_MODE, 1099 | name="Fan Mode", 1100 | entity_category=EntityCategory.CONFIG, 1101 | icon='mdi:fan-speed-1', 1102 | options=["Fixed", "Down", "Up", "Both", "Side"], 1103 | options_value=["0", "1", "2", "3", "4"] 1104 | ) 1105 | ) 1106 | 1107 | ERV_SELECTS: tuple[PanasonicSelectDescription, ...] = ( 1108 | PanasonicSelectDescription( 1109 | key=ERV_VENTILATE_MODE, 1110 | name="Ventilate Mode", 1111 | entity_category=EntityCategory.CONFIG, 1112 | icon='mdi:home-thermometer', 1113 | options=["Auto", "Ventilate", "Normal"], 1114 | options_value=["0", "1", "2"], 1115 | ), 1116 | PanasonicSelectDescription( 1117 | key=ERV_PRE_HEAT_COOL, 1118 | name="Pre Head/Cool", 1119 | entity_category=EntityCategory.CONFIG, 1120 | icon='mdi:home-thermometer-outline', 1121 | options=["Disabled", "30min", "60min"], 1122 | options_value=["0", "1", "2"] 1123 | ) 1124 | ) 1125 | 1126 | FRIDGE_SELECTS: tuple[PanasonicSelectDescription, ...] = ( 1127 | PanasonicSelectDescription( 1128 | key=FRIDGE_FREEZER_MODE, 1129 | name="Freezer mode", 1130 | entity_category=EntityCategory.CONFIG, 1131 | icon='mdi:fridge-top', 1132 | options=["Weak", "Medium", "Strong"], 1133 | options_value=["0", "2", "4"], 1134 | ), 1135 | PanasonicSelectDescription( 1136 | key=FRIDGE_CHAMBER_MODE, 1137 | name="Chamber Mode", 1138 | entity_category=EntityCategory.CONFIG, 1139 | icon='mdi:fridge-bottom', 1140 | options=["Weak", "Medium", "Strong"], 1141 | options_value=["0", "2", "4"], 1142 | ), 1143 | PanasonicSelectDescription( 1144 | key=FRIDGE_THAW_MODE, 1145 | name="Thaw Mode", 1146 | entity_category=EntityCategory.CONFIG, 1147 | icon='mdi:fridge-outline', 1148 | options=["Weak", "Medium", "Strong"], 1149 | options_value=["0", "2", "4"], 1150 | ) 1151 | ) 1152 | 1153 | WASHING_MACHINE_SELECTS: tuple[PanasonicSelectDescription, ...] = ( 1154 | PanasonicSelectDescription( 1155 | key=WASHING_MACHINE_PROGRESS, 1156 | name="Progress", 1157 | entity_category=EntityCategory.CONFIG, 1158 | icon='mdi:washing-machine', 1159 | options=["Standard", "Soft Wash", "Strong Wash", "Wash with Shirt", "Wash with Blanket", "Wash with High-end clothing", "Wash with Woolen fabrics", "User-defined Wash", "Soak Wash", "Dry Clean", "Quick Wash", "Tank Wash", "Wash with Warm Water"], 1160 | options_value=["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"] 1161 | ), 1162 | PanasonicSelectDescription( 1163 | key=WASHING_MACHINE_TIMER, 1164 | name="Appointment Time", 1165 | entity_category=EntityCategory.CONFIG, 1166 | icon='mdi:clock', 1167 | options=["0", "1", "2", "3"], 1168 | options_value=["0", "1", "2", "3"] 1169 | ), 1170 | PanasonicSelectDescription( 1171 | key=WASHING_MACHINE_POSTPONE_DRYING, 1172 | name="Postpone Drying", 1173 | entity_category=EntityCategory.CONFIG, 1174 | icon='mdi:clock', 1175 | options=["Off", "1", "2", "3", "4", "5", "6", "7", "8"], 1176 | options_value=["0", "1", "2", "3", "4", "5", "6", "7", "8"] 1177 | ) 1178 | ) 1179 | 1180 | 1181 | @dataclass 1182 | class PanasonicSensorDescription( 1183 | SensorEntityDescription 1184 | ): 1185 | """Class to describe an Panasonic sensor.""" 1186 | 1187 | 1188 | AIRPURIFIER_SENSORS: tuple[PanasonicSensorDescription, ...] = ( 1189 | PanasonicSensorDescription( 1190 | key=AIRPURIFIER_AIR_QUALITY, 1191 | name="Air Quality", 1192 | device_class= SensorDeviceClass.AQI, 1193 | icon='mdi:leaf' 1194 | ), 1195 | PanasonicSensorDescription( 1196 | key=AIRPURIFIER_PM25, 1197 | name="PM2.5", 1198 | native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 1199 | state_class=SensorStateClass.MEASUREMENT, 1200 | device_class=SensorDeviceClass.PM25, 1201 | icon="mdi:chemical-weapon" 1202 | ), 1203 | PanasonicSensorDescription( 1204 | key=AIRPURIFIER_FORMALDEHYDE, 1205 | name="Formaldehyde", 1206 | native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, 1207 | state_class=SensorStateClass.MEASUREMENT, 1208 | # device_class=SensorDeviceClass.PM25, 1209 | icon="mdi:chemical-weapon" 1210 | ), 1211 | PanasonicSensorDescription( 1212 | key=AIRPURIFIER_RUNNING_TIME, 1213 | name="Running Time", 1214 | native_unit_of_measurement=UnitOfTime.MINUTES, 1215 | state_class=SensorStateClass.MEASUREMENT, 1216 | icon="mdi:clock-outline" 1217 | ), 1218 | PanasonicSensorDescription( 1219 | key=AIRPURIFIER_ERROR_CODE, 1220 | name="Error Code", 1221 | icon="mdi:alert-circle" 1222 | ), 1223 | PanasonicSensorDescription( 1224 | key=AIRPURIFIER_ENERGY, 1225 | name="Energy", 1226 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 1227 | state_class=SensorStateClass.TOTAL_INCREASING, 1228 | device_class=SensorDeviceClass.ENERGY, 1229 | icon="mdi:flash" 1230 | ) 1231 | ) 1232 | 1233 | CLIMATE_SENSORS: tuple[PanasonicSensorDescription, ...] = ( 1234 | PanasonicSensorDescription( 1235 | key=CLIMATE_TEMPERATURE_INDOOR, 1236 | name="Inside Temperature", 1237 | native_unit_of_measurement=UnitOfTemperature.CELSIUS, 1238 | state_class=SensorStateClass.MEASUREMENT, 1239 | device_class=SensorDeviceClass.TEMPERATURE, 1240 | icon="mdi:thermometer" 1241 | ), 1242 | PanasonicSensorDescription( 1243 | key=CLIMATE_ERROR_CODE, 1244 | name="Error Code", 1245 | icon="mdi:alert-circle" 1246 | ), 1247 | PanasonicSensorDescription( 1248 | key=CLIMATE_TEMPERATURE_OUTDOOR, 1249 | name="Outside Temperature", 1250 | native_unit_of_measurement=UnitOfTemperature.CELSIUS, 1251 | state_class=SensorStateClass.MEASUREMENT, 1252 | device_class=SensorDeviceClass.TEMPERATURE, 1253 | icon="mdi:thermometer" 1254 | ), 1255 | PanasonicSensorDescription( 1256 | key=CLIMATE_PM25, 1257 | name="PM2.5", 1258 | native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 1259 | state_class=SensorStateClass.MEASUREMENT, 1260 | device_class=SensorDeviceClass.PM25, 1261 | icon="mdi:chemical-weapon" 1262 | ), 1263 | PanasonicSensorDescription( 1264 | key=CLIMATE_ENERGY, 1265 | name="Energy", 1266 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 1267 | state_class=SensorStateClass.TOTAL_INCREASING, 1268 | device_class=SensorDeviceClass.ENERGY, 1269 | icon="mdi:flash" 1270 | ) 1271 | ) 1272 | 1273 | DEHUMIDIFIER_SENSORS: tuple[PanasonicSensorDescription, ...] = ( 1274 | PanasonicSensorDescription( 1275 | key=DEHUMIDIFIER_HUMIDITY_INDOOR, 1276 | name="Indoor Humidity", 1277 | native_unit_of_measurement=PERCENTAGE, 1278 | state_class=SensorStateClass.MEASUREMENT, 1279 | device_class=SensorDeviceClass.HUMIDITY, 1280 | icon="mdi:water-percent" 1281 | ), 1282 | PanasonicSensorDescription( 1283 | key=DEHUMIDIFIER_PM10, 1284 | name="PM10", 1285 | native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 1286 | state_class=SensorStateClass.MEASUREMENT, 1287 | device_class=SensorDeviceClass.PM10, 1288 | icon="mdi:chemical-weapon" 1289 | ), 1290 | PanasonicSensorDescription( 1291 | key=DEHUMIDIFIER_PM25, 1292 | name="PM2.5", 1293 | native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 1294 | state_class=SensorStateClass.MEASUREMENT, 1295 | device_class=SensorDeviceClass.PM25, 1296 | icon="mdi:chemical-weapon" 1297 | ), 1298 | PanasonicSensorDescription( 1299 | key=DEHUMIDIFIER_ENERGY, 1300 | name="Energy", 1301 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 1302 | state_class=SensorStateClass.TOTAL_INCREASING, 1303 | device_class=SensorDeviceClass.ENERGY, 1304 | icon="mdi:flash" 1305 | ), 1306 | PanasonicSensorDescription( 1307 | key=DEHUMIDIFIER_ERROR_CODE, 1308 | name="Error Code", 1309 | icon="mdi:alert-circle" 1310 | ) 1311 | ) 1312 | 1313 | ERV_SENSORS: tuple[PanasonicSensorDescription, ...] = ( 1314 | PanasonicSensorDescription( 1315 | key=ERV_TEMPERATURE_IN, 1316 | name="Temperature In", 1317 | native_unit_of_measurement=UnitOfTemperature.CELSIUS, 1318 | state_class=SensorStateClass.MEASUREMENT, 1319 | device_class=SensorDeviceClass.TEMPERATURE, 1320 | icon="mdi:thermometer" 1321 | ), 1322 | PanasonicSensorDescription( 1323 | key=ERV_TEMPERATURE_OUT, 1324 | name="Temperature Outdoor", 1325 | native_unit_of_measurement=UnitOfTemperature.CELSIUS, 1326 | state_class=SensorStateClass.MEASUREMENT, 1327 | device_class=SensorDeviceClass.TEMPERATURE, 1328 | icon="mdi:thermometer" 1329 | ), 1330 | PanasonicSensorDescription( 1331 | key=ERV_ENERGY, 1332 | name="Energy", 1333 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 1334 | state_class=SensorStateClass.TOTAL_INCREASING, 1335 | device_class=SensorDeviceClass.ENERGY, 1336 | icon="mdi:flash" 1337 | ), 1338 | PanasonicSensorDescription( 1339 | key=ERV_ERROR_CODE, 1340 | name="Error Code", 1341 | icon="mdi:alert-circle" 1342 | ) 1343 | ) 1344 | 1345 | FRIDGE_SENSORS: tuple[PanasonicSensorDescription, ...] = ( 1346 | PanasonicSensorDescription( 1347 | key=FRIDGE_FREEZER_TEMPERATURE, 1348 | name="Freezer Temperature", 1349 | native_unit_of_measurement=UnitOfTemperature.CELSIUS, 1350 | state_class=SensorStateClass.MEASUREMENT, 1351 | device_class=SensorDeviceClass.TEMPERATURE, 1352 | icon='mdi:fridge-top' 1353 | ), 1354 | PanasonicSensorDescription( 1355 | key=FRIDGE_CHAMBER_TEMPERATURE, 1356 | name="Chamber Temperature", 1357 | native_unit_of_measurement=UnitOfTemperature.CELSIUS, 1358 | state_class=SensorStateClass.MEASUREMENT, 1359 | device_class=SensorDeviceClass.TEMPERATURE, 1360 | icon="mdi:fridge-bottom" 1361 | ), 1362 | PanasonicSensorDescription( 1363 | key=FRIDGE_ENERGY, 1364 | name="Energy", 1365 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 1366 | state_class=SensorStateClass.TOTAL_INCREASING, 1367 | device_class=SensorDeviceClass.ENERGY, 1368 | icon="mdi:flash" 1369 | ), 1370 | PanasonicSensorDescription( 1371 | key=FRIDGE_THAW_TEMPERATURE, 1372 | name="Thaw Temperature", 1373 | native_unit_of_measurement=UnitOfTemperature.CELSIUS, 1374 | state_class=SensorStateClass.MEASUREMENT, 1375 | device_class=SensorDeviceClass.TEMPERATURE, 1376 | icon="mdi:fridge-outline" 1377 | ), 1378 | PanasonicSensorDescription( 1379 | key=FRIDGE_ERROR_CODE, 1380 | name="Error Code", 1381 | icon="mdi:alert-circle" 1382 | ), 1383 | PanasonicSensorDescription( 1384 | key=FRIDGE_ERROR_CODE_JP, 1385 | name="Error Code", 1386 | icon="mdi:alert-circle" 1387 | ), 1388 | PanasonicSensorDescription( 1389 | key=ENTITY_DOOR_OPENS, 1390 | name="Monthly Door Open Times", 1391 | icon="mdi:information-slab-symbol" 1392 | ), 1393 | PanasonicSensorDescription( 1394 | key=ENTITY_MONTHLY_ENERGY, 1395 | name="Monthly Energy", 1396 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 1397 | state_class=SensorStateClass.TOTAL_INCREASING, 1398 | device_class=SensorDeviceClass.ENERGY, 1399 | icon="mdi:flash" 1400 | ) 1401 | ) 1402 | 1403 | LIGHT_SENSORS: tuple[PanasonicSensorDescription, ...] = ( 1404 | PanasonicSensorDescription( 1405 | key=LIGHT_OPERATION_STATE, 1406 | name="Operation Mode", 1407 | icon='mdi:dip-switch', 1408 | # options=["All Off", "Channel 1 On", "Channel 2 On", "Channel 1, 2 On", "Channel 3 On", "Channel 1, 3 On", "Channel 2, 3 On", "All On"], 1409 | # options_value=["0", "1", "2", "3", "4", "5", "6", "7"], 1410 | ), 1411 | PanasonicSensorDescription( 1412 | key=LIGHT_RESERVED, 1413 | name="Reserved", 1414 | icon='mdi:help' 1415 | ) 1416 | ) 1417 | 1418 | WASHING_MACHINE_SENSORS: tuple[PanasonicSensorDescription, ...] = ( 1419 | PanasonicSensorDescription( 1420 | key=WASHING_MACHINE_REMAING_WASH_TIME, 1421 | name="Washing Remaining Time", 1422 | state_class=SensorStateClass.MEASUREMENT, 1423 | native_unit_of_measurement=UnitOfTime.MINUTES, 1424 | icon="mdi:clock" 1425 | ), 1426 | PanasonicSensorDescription( 1427 | key=WASHING_MACHINE_TIMER_REMAINING_TIME, 1428 | name="Timer Remaining Time", 1429 | state_class=SensorStateClass.MEASUREMENT, 1430 | native_unit_of_measurement=UnitOfTime.MINUTES, 1431 | icon="mdi:clock-outline" 1432 | ), 1433 | PanasonicSensorDescription( 1434 | key=WASHING_MACHINE_ERROR_CODE, 1435 | name="Error Code", 1436 | icon="mdi:alert-circle" 1437 | ), 1438 | PanasonicSensorDescription( 1439 | key=WASHING_MACHINE_CURRENT_MODE, 1440 | name="Current Mode", 1441 | device_class=SensorDeviceClass.ENUM, 1442 | icon="mdi:washing-machine" 1443 | ), 1444 | PanasonicSensorDescription( 1445 | key=WASHING_MACHINE_CURRENT_PROGRESS, 1446 | name="Current Progress", 1447 | device_class=SensorDeviceClass.ENUM, 1448 | icon="mdi:progress-helper" 1449 | ), 1450 | PanasonicSensorDescription( 1451 | key=WASHING_MACHINE_OPERATING_STATUS, 1452 | name="Operating Status", 1453 | device_class=SensorDeviceClass.ENUM, 1454 | icon="mdi:washing-machine" 1455 | ), 1456 | PanasonicSensorDescription( 1457 | key=ENTITY_WASH_TIMES, 1458 | name="Monthly Washing Times", 1459 | icon="mdi:information-slab-symbol" 1460 | ), 1461 | PanasonicSensorDescription( 1462 | key=ENTITY_WATER_USED, 1463 | name="Monthly Used Water", 1464 | state_class=SensorStateClass.MEASUREMENT, 1465 | native_unit_of_measurement=UnitOfVolume.LITERS, 1466 | icon="mdi:water" 1467 | ), 1468 | PanasonicSensorDescription( 1469 | key=WASHING_MACHINE_ENERGY, 1470 | name="Energy", 1471 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 1472 | state_class=SensorStateClass.TOTAL_INCREASING, 1473 | device_class=SensorDeviceClass.ENERGY, 1474 | icon="mdi:flash" 1475 | ), 1476 | PanasonicSensorDescription( 1477 | key=WASHING_MACHINE_REMOTE_CONTROL, 1478 | name="Remote Control", 1479 | icon='mdi:cog' 1480 | ) 1481 | ) 1482 | 1483 | WEIGHT_PLATE_SENSORS: tuple[PanasonicSensorDescription, ...] = ( 1484 | PanasonicSensorDescription( 1485 | key=WEIGHT_PLATE_FOOD_NAME, 1486 | name="Food name", 1487 | icon="mdi:food" 1488 | ), 1489 | PanasonicSensorDescription( 1490 | key=WEIGHT_PLATE_BUY_DATE, 1491 | name="Buy Date", 1492 | device_class=SensorDeviceClass.TIMESTAMP, 1493 | icon="mdi:clock" 1494 | ), 1495 | PanasonicSensorDescription( 1496 | key=WEIGHT_PLATE_DUE_DATE, 1497 | name="Due Date", 1498 | device_class=SensorDeviceClass.TIMESTAMP, 1499 | icon="mdi:clock-outline" 1500 | ), 1501 | PanasonicSensorDescription( 1502 | key=WEIGHT_PLATE_MANAGEMENT_MODE, 1503 | name="Management Mode", 1504 | icon="mdi:cog" 1505 | ), 1506 | PanasonicSensorDescription( 1507 | key=WEIGHT_PLATE_MANAGEMENT_VALUE, 1508 | name="Management Value", 1509 | icon="mdi:cog" 1510 | ), 1511 | PanasonicSensorDescription( 1512 | key=WEIGHT_PLATE_AMOUNT_MAX, 1513 | name="Max Amount", 1514 | icon="mdi:cog" 1515 | ), 1516 | PanasonicSensorDescription( 1517 | key=WEIGHT_PLATE_COMMUNICATION_MODE, 1518 | name="Communication Mode", 1519 | icon="mdi:cog" 1520 | ), 1521 | PanasonicSensorDescription( 1522 | key=WEIGHT_PLATE_COMMUNICATION_TIME, 1523 | name="Communication Time", 1524 | icon="mdi:clock-outline" 1525 | ), 1526 | PanasonicSensorDescription( 1527 | key=WEIGHT_PLATE_TOTAL_WEIGHT, 1528 | name="Total Weight", 1529 | device_class=SensorDeviceClass.WEIGHT, 1530 | native_unit_of_measurement=UnitOfMass.GRAMS, 1531 | state_class=SensorStateClass.MEASUREMENT, 1532 | icon="mdi:weight-gram" 1533 | ), 1534 | PanasonicSensorDescription( 1535 | key=WEIGHT_PLATE_RESTORE_WEIGHT, 1536 | name="Restore Weight", 1537 | device_class=SensorDeviceClass.WEIGHT, 1538 | native_unit_of_measurement=UnitOfMass.GRAMS, 1539 | state_class=SensorStateClass.MEASUREMENT, 1540 | icon="mdi:weight-gram" 1541 | ), 1542 | PanasonicSensorDescription( 1543 | key=WEIGHT_PLATE_LOW_BATTERY, 1544 | name="Low Battery", 1545 | icon="mdi:battery-alert" 1546 | ) 1547 | ) 1548 | 1549 | @dataclass 1550 | class PanasonicSwitchDescription( 1551 | SwitchEntityDescription 1552 | ): 1553 | """Class to describe an Panasonic switch.""" 1554 | 1555 | 1556 | AIRPURIFIER_SWITCHES: tuple[PanasonicSwitchDescription, ...] = ( 1557 | PanasonicSwitchDescription( 1558 | key=AIRPURIFIER_RESET_FILTER_NOTIFY, 1559 | name="Reset Filter Notify", 1560 | device_class=SwitchDeviceClass.SWITCH, 1561 | icon='mdi:filter-remove' 1562 | ), 1563 | PanasonicSwitchDescription( 1564 | key=AIRPURIFIER_BUZZER, 1565 | name="Buzzer", 1566 | device_class=SwitchDeviceClass.SWITCH, 1567 | icon='mdi:volume-high' 1568 | ), 1569 | PanasonicSwitchDescription( 1570 | key=AIRPURIFIER_PET_MODE, 1571 | name="Pet Mode", 1572 | device_class=SwitchDeviceClass.SWITCH, 1573 | icon='mdi:paw' 1574 | ) 1575 | ) 1576 | 1577 | CLIMATE_SWITCHES: tuple[PanasonicSwitchDescription, ...] = ( 1578 | PanasonicSwitchDescription( 1579 | key=CLIMATE_AIRFRESH_MODE, 1580 | name=" nanoe™ X", 1581 | device_class=SwitchDeviceClass.SWITCH, 1582 | icon='mdi:atom-variant' 1583 | ), 1584 | PanasonicSwitchDescription( 1585 | key=CLIMATE_ANTI_MILDEW, 1586 | name="Anti Mildew", 1587 | device_class=SwitchDeviceClass.SWITCH, 1588 | icon='mdi:weather-dust' 1589 | ), 1590 | PanasonicSwitchDescription( 1591 | key=CLIMATE_AUTO_CLEAN, 1592 | name="Auto Clean", 1593 | device_class=SwitchDeviceClass.SWITCH, 1594 | icon='mdi:broom' 1595 | ), 1596 | PanasonicSwitchDescription( 1597 | key=CLIMATE_BUZZER, 1598 | name="Buzzer", 1599 | device_class=SwitchDeviceClass.SWITCH, 1600 | icon='mdi:volume-source' 1601 | ), 1602 | PanasonicSwitchDescription( 1603 | key=CLIMATE_MONITOR_MILDEW, 1604 | name="Mildew Monitor", 1605 | device_class=SwitchDeviceClass.SWITCH, 1606 | icon='mdi:mushroom' 1607 | ) 1608 | ) 1609 | 1610 | DEHUMIDIFIER_SWITCHES: tuple[PanasonicSwitchDescription, ...] = ( 1611 | PanasonicSwitchDescription( 1612 | key=DEHUMIDIFIER_AIRFRESH_MODE, 1613 | name=" nanoe™ X", 1614 | device_class=SwitchDeviceClass.SWITCH, 1615 | icon='mdi:atom-variant' 1616 | ), 1617 | PanasonicSwitchDescription( 1618 | key=DEHUMIDIFIER_BUZZER, 1619 | name="Buzzer", 1620 | device_class=SwitchDeviceClass.SWITCH, 1621 | icon='mdi:volume-high' 1622 | ) 1623 | ) 1624 | 1625 | FRIDGE_SWITCHES: tuple[PanasonicSwitchDescription, ...] = ( 1626 | PanasonicSwitchDescription( 1627 | key=FRIDGE_DEFROST_SETTING, 1628 | name=" nanoe™ X", 1629 | device_class=SwitchDeviceClass.SWITCH, 1630 | icon='mdi:snowflake-melt' 1631 | ), 1632 | PanasonicSwitchDescription( 1633 | key=FRIDGE_ECO, 1634 | name="ECO", 1635 | device_class=SwitchDeviceClass.SWITCH, 1636 | icon='mdi:sprout' 1637 | ), 1638 | PanasonicSwitchDescription( 1639 | key=FRIDGE_NANOEX, 1640 | name=" nanoe™ X", 1641 | device_class=SwitchDeviceClass.SWITCH, 1642 | icon='mdi:atom-variant' 1643 | ), 1644 | PanasonicSwitchDescription( 1645 | key=FRIDGE_STOP_ICE_MAKING, 1646 | name="Stop Ice Making", 1647 | device_class=SwitchDeviceClass.SWITCH, 1648 | icon='mdi:snowflake' 1649 | ), 1650 | PanasonicSwitchDescription( 1651 | key=FRIDGE_FAST_ICE_MAKING, 1652 | name="Fast Ice Making", 1653 | device_class=SwitchDeviceClass.SWITCH, 1654 | icon='mdi:snowflake' 1655 | ), 1656 | PanasonicSwitchDescription( 1657 | key=FRIDGE_FRESH_QUICK_FREZZE, 1658 | name="Fresh Quick Freeze", 1659 | device_class=SwitchDeviceClass.SWITCH, 1660 | icon='mdi:snowflake-check' 1661 | ), 1662 | PanasonicSwitchDescription( 1663 | key=FRIDGE_WINTER_MDOE, 1664 | name="Winter Mode", 1665 | device_class=SwitchDeviceClass.SWITCH, 1666 | icon='mdi:snowman' 1667 | ), 1668 | PanasonicSwitchDescription( 1669 | key=FRIDGE_SHOPPING_MODE, 1670 | name="Shopping Mode", 1671 | device_class=SwitchDeviceClass.SWITCH, 1672 | icon='mdi:shopping' 1673 | ), 1674 | PanasonicSwitchDescription( 1675 | key=FRIDGE_GO_OUT_MODE, 1676 | name="Go Out Mode", 1677 | device_class=SwitchDeviceClass.SWITCH, 1678 | icon='mdi:logout' 1679 | ) 1680 | ) 1681 | 1682 | LIGHT_SWITCHES: tuple[PanasonicSwitchDescription, ...] = ( 1683 | PanasonicSwitchDescription( 1684 | key=LIGHT_POWER, 1685 | name="Switch", 1686 | device_class=SwitchDeviceClass.SWITCH, 1687 | icon='mdi:toggle-switch' 1688 | ), 1689 | PanasonicSwitchDescription( 1690 | key=LIGHT_MAINTAIN_MODE, 1691 | name="Maintain Mode", 1692 | device_class=SwitchDeviceClass.SWITCH, 1693 | entity_category=EntityCategory.CONFIG, 1694 | icon='mdi:swap-horizontal' 1695 | ), 1696 | PanasonicSwitchDescription( 1697 | key=LIGHT_RESERVED, 1698 | name="Reserved", 1699 | device_class=SwitchDeviceClass.SWITCH, 1700 | icon='mdi:help' 1701 | ) 1702 | ) 1703 | 1704 | WASHING_MACHINE_SWITCHES: tuple[PanasonicSwitchDescription, ...] = ( 1705 | PanasonicSwitchDescription( 1706 | key=WASHING_MACHINE_ENABLE, 1707 | name="Pause/Start", 1708 | device_class=SwitchDeviceClass.SWITCH, 1709 | icon='mdi:play-pause' 1710 | ), 1711 | PanasonicSwitchDescription( 1712 | key=WASHING_MACHINE_WARM_WATER, 1713 | name="Warm Water", 1714 | device_class=SwitchDeviceClass.SWITCH, 1715 | icon='mdi:heat-wave' 1716 | ) 1717 | ) 1718 | 1719 | SAA_BINARY_SENSORS = { 1720 | DEVICE_TYPE_AIRPURIFIER: AIRPURIFIER_BINARY_SENSORS, 1721 | DEVICE_TYPE_CLIMATE: CLIMATE_BINARY_SENSORS, 1722 | DEVICE_TYPE_DEHUMIDIFIER: DEHUMIDIFIER_BINARY_SENSORS, 1723 | DEVICE_TYPE_ERV: ERV_BINARY_SENSORS, 1724 | DEVICE_TYPE_FRIDGE: FRIDGE_BINARY_SENSORS, 1725 | DEVICE_TYPE_LIGHT: LIGHT_BINARY_SENSORS, 1726 | DEVICE_TYPE_WASHING_MACHINE: WASHING_MACHINE_BINARY_SENSORS 1727 | } 1728 | 1729 | SAA_NUMBERS = { 1730 | DEVICE_TYPE_CLIMATE: CLIMATE_NUMBERS, 1731 | DEVICE_TYPE_DEHUMIDIFIER: DEHUMIDIFIER_NUMBERS, 1732 | DEVICE_TYPE_ERV: ERV_NUMBERS, 1733 | DEVICE_TYPE_LIGHT: LIGHT_NUMBERS 1734 | } 1735 | 1736 | SAA_SELECTS = { 1737 | DEVICE_TYPE_AIRPURIFIER: AIRPURIFIER_SELECTS, 1738 | DEVICE_TYPE_CLIMATE: CLIMATE_SELECTS, 1739 | DEVICE_TYPE_DEHUMIDIFIER: DEHUMIDIFIER_SELECTS, 1740 | DEVICE_TYPE_ERV: ERV_SELECTS, 1741 | DEVICE_TYPE_FRIDGE: FRIDGE_SELECTS 1742 | } 1743 | 1744 | SAA_SENSORS = { 1745 | DEVICE_TYPE_AIRPURIFIER: AIRPURIFIER_SENSORS, 1746 | DEVICE_TYPE_CLIMATE: CLIMATE_SENSORS, 1747 | DEVICE_TYPE_DEHUMIDIFIER: DEHUMIDIFIER_SENSORS, 1748 | DEVICE_TYPE_ERV: ERV_SENSORS, 1749 | DEVICE_TYPE_FRIDGE: FRIDGE_SENSORS, 1750 | DEVICE_TYPE_LIGHT: LIGHT_SENSORS 1751 | } 1752 | 1753 | SAA_SWITCHES = { 1754 | DEVICE_TYPE_AIRPURIFIER: AIRPURIFIER_SWITCHES, 1755 | DEVICE_TYPE_CLIMATE: CLIMATE_SWITCHES, 1756 | DEVICE_TYPE_DEHUMIDIFIER: DEHUMIDIFIER_SWITCHES, 1757 | DEVICE_TYPE_FRIDGE: FRIDGE_SWITCHES, 1758 | DEVICE_TYPE_LIGHT: LIGHT_SWITCHES 1759 | } 1760 | --------------------------------------------------------------------------------