├── 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 | [](https://github.com/hacs/integration)
2 | 
3 | [](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 |
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 | |
|
|
|
--------------------------------------------------------------------------------
/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 | [](https://github.com/hacs/integration)
2 | 
3 | [](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 |
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 | |
|
|
|
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 |
--------------------------------------------------------------------------------