Showing preview only (306K chars total). Download the full file or copy to clipboard to get everything.
Repository: scottyphillips/echonetlite_homeassistant
Branch: master
Commit: 0995f2817b74
Files: 32
Total size: 287.1 KB
Directory structure:
gitextract_9lbvaute/
├── .github/
│ └── workflows/
│ ├── black.yml
│ └── python-app.yml
├── .gitignore
├── .vscode/
│ └── settings.json
├── LICENSE
├── README.ja.md
├── README.md
├── Services.ja.md
├── Services.md
├── custom_components/
│ └── echonetlite/
│ ├── __init__.py
│ ├── binary_sensor.py
│ ├── climate.py
│ ├── config_flow.py
│ ├── const.py
│ ├── cover.py
│ ├── fan.py
│ ├── light.py
│ ├── manifest.json
│ ├── number.py
│ ├── quirks/
│ │ ├── Nichicon/
│ │ │ └── all/
│ │ │ └── 02A5.py
│ │ └── Panasonic/
│ │ └── all/
│ │ └── 0135.py
│ ├── select.py
│ ├── sensor.py
│ ├── services.yaml
│ ├── strings.json
│ ├── switch.py
│ ├── time.py
│ └── translations/
│ ├── en.json
│ ├── ja.json
│ └── pt.json
├── hacs.json
└── info.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/black.yml
================================================
name: Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: psf/black@stable
================================================
FILE: .github/workflows/python-app.yml
================================================
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: Check for Syntax Errors
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
# - name: Install dependencies
# run: |
# python -m pip install --upgrade pip
# pip install flake8 pytest
# if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
# - name: Lint with flake8
# run: |
# stop the build if there are Python syntax errors or undefined names
# flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
# - name: Test with pytest
# run: |
# pytest
================================================
FILE: .gitignore
================================================
*.pyc
*.tar.gz
build/*
dist/*
pychonet.egg-info/*
.DS_Store
__pycache__
pychonet
================================================
FILE: .vscode/settings.json
================================================
{
"python.formatting.provider": "black"
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 Scott Phillips
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.ja.md
================================================
# ECHONETLite Platform Custom Component for Home Assistant
[![GitHub Release][releases-shield]][releases]
[![License][license-shield]](LICENSE)
[![hacs][hacsbadge]][hacs]
![Project Maintenance][maintenance-shield]
ECHONETLite互換機器で使用するためのHomeAssistantカスタムコンポーネント。
このカスタムコンポーネントは「pychonet」Python3ライブラリを利用しています。
また、そのコンポーネントもこの作者によって維持されています。
(https://github.com/scottyphillips/pychonet)
**このコンポーネントは、エアコン・ファン・センサー・選択およびスイッチプラットフォームをセットアップします**
# 現在動作確認されている機器:
フィードバックに基づいて、このカスタムコンポーネントは以下の互換性のあるECHONETLite機器で動作します:
* 三菱電機 MAC-568IF-E WiFi アダプターでコントロールされている以下の機器:
* GE シリーズ
* MSZ-GE42VAD
* MSZ-GE24VAD
* MSZ-GL71VGD
* MSZ-GL50VGD
* MSZ-GL35VGD
* MSZ-GL25VGD
* AP シリーズ
* MSZ-AP22VGD
* MSZ-AP25VGD
* MSZ-AP50VGD
* LN シリーズ
* MSZ-LN25VG2
* MSZ-LN35VG2
* MSZ-LN50VG2
* 換気システム
* PEA-M100GAA
* PEA-RP140GAA
* 三菱電機 HM-W002-AC WiFi アダプターでコントロールされている以下の機器:
* JXV シリーズ
* MSZ-JXV4018S
* 三菱電機 MAC-578IF2-E WiFi アダプターでコントロールされている以下の機器:
* AP シリーズ
* MSZ-AP22VGD
* MSZ-AP35VGD
* MSZ-AP50VGD
* 換気システム
* PEAD-RP71
* 三菱電機 MAC-600IF WiFi アダプターでコントロールされている以下の機器:
* Z シリーズ
* MSZ-ZW4022S
* 三菱電機 MAC-900IF WiFi アダプターでコントロールされている以下の機器:
* Z シリーズ
* MSZ-ZW4024S
* XD シリーズ
* MSZ-XD2225
* R シリーズ
* MSZ-BKR2223
* 三菱電機 REF-WLAN001 アダプターでコントロールされている以下の機器:
* 冷蔵庫
* MR-WZ55H
* 富士通ゼネラル OP-J03DZ WiFi アダプターでコントロールされている以下の機器:
* エアコン
* 「ノクリア」Cシリーズ
* AS-C224R
* AS-C254R
* 「ノクリア」Vシリーズ
* AS-V173N2
* 'MoekadenRoom' ECHONETLite シミュレーター: https://github.com/SonyCSL/MoekadenRoom
* エアコンオブジェクト
* 照明オブジェクト
* 電動ブラインドオブジェクト
* 電子錠オブジェクト
* 温度計オブジェクト
* スマートメーターオブジェクト
* シャープ
* エアコン
* AY-J22H
* AY-L40P
* 加湿空気清浄機
* KI-HS70
* 太陽光発電システム
* JH-RWL8
* パナソニック
* エアコン
* CS-221DJ
* IP/JEM-A 変換アダプター
* HF-JA2-W
* コイズミ照明
* スマートブリッジ AE50264E (https://www.koizumi-lt.co.jp/product/jyutaku/tree/ )
* リンナイ
* 給湯器 (Wi-Fi機能(ECHONETLite)搭載のリモコン)
* スイッチエンティティで、運転・ふろ自動・おいだき・タイマー予約をコントロール可能
* タイマー予約を設定するための入力エンティティは、テンプレートと[サービスコール]((Services.ja.md))を使用して設定できます。
* ダイキン
* エアコン
* ECHONET Lite 対応機器
* オムロン
* 太陽光発電用ゲートウェイ
* 太陽光発電とグリッド消費を含むホームアシスタントエネルギーダッシュボードの完全サポート。
* 各種センサーの負荷状態。
* 低圧スマートメーター(Bルートサービス)
* Wi-SUN <-> Ethernet/Wifi ブリッジが必要
* [nao-pon/python-echonet-lite](https://github.com/nao-pon/python-echonet-lite)
* など
上記の他にも[ECHONET Lite規格](https://echonet.jp/product/echonet-lite/)にリストアップされている多くの機器がコントロールできる可能性があります。
## インストール - ECHONETプロトコルを有効にする
このカスタムコンポーネントは、もともと三菱 MAC-568IF-E WiFi アダプター用に設計されました。
ECHONETliteを有効にするための基本ガイドを以下に示します。
公式の Mitsubishi AU/NZ Wifi アプリで「edit unit」設定の「ECHONETlite」プロトコル有効にする必要があります。
※ 日本で発売されている ECHONET Lite 対応のアダプター(HM-W002-AC, HM-W002-ACB など)では設定の必要はないかも知れません。
![echonet][echonetimg]
他の多くの製品はこのカスタムコンポーネントを使用して動作しますが、「ECHONETlite」プロトコルを正しくサポートしている必要があります。 作者は、他のベンダー製品で ECHONET Lite を有効にすることを支援することはできません。
## インストール
### HACS利用
1. 統合でECHONETLiteを検索します
2. [インストール]をクリックします
### ダウンロードして配置
1. 選択したツールを使用して、HA構成( `configuration.yaml`がある場所)のディレクトリ(フォルダー)を開きます。
2. そこに`custom_components`ディレクトリ(フォルダ)がない場合は、それを作成する必要があります。
3. `custom_components`ディレクトリ(フォルダ)に`echonetlite`という新しいフォルダを作成します。
4. このリポジトリの`custom_components/echonetlite/`ディレクトリ(フォルダ)から_すべての_ファイルをダウンロードします。
5. ダウンロードしたファイルを作成した新しいディレクトリ(フォルダ)に配置します。
## コンポーネントの有効化
1. ホームアシスタントを再起動し、ブラウザのキャッシュをクリアします
2. 「構成」->「統合」->「統合の追加」に移動します。
3. 「echonetlite」統合を選択します。 ホストフィールドにIPアドレスを入力し、プラットフォームに名前を付けます。
4. プラットフォームは、サポートされているプラットフォームを自動的に構成します。 気候、センサー、スイッチ、ファン、選択。
5. 構成する追加のデバイスがある場合は、手順2を繰り返します。
## サポートされているエアコンおよび空気清浄機の風量および風向スイングモード設定のオプションの構成
統合を追加したら、構成->統合に戻ることができます。
ECHONETLiteデバイスの下で[構成]をクリックします。
必要な風量と風向スイングモードの設定を微調整します。 設定では、統合によりあなたのシステムでサポートされている機能を判別できます。
注:ECHONETLiteはこれらの設定に対して許可された値を返す手段を提供しないため、適切な特定のオプションを選択することは「試行錯誤」のプロセスです。
オプションを構成して保存するとすぐに、設定が有効になります。
## 栄誉殿堂
スイッチエンティティを作成し、カスタムサービスコールフレームワークを作成してくれた Naoki Sawada (nao-pon) に感謝します。
コードベースとドキュメントの全体的な質の向上してくれた JasonNader に感謝します。
古いコードのリファクタリングとテストを手伝ってくれた khcnz (Karl Chaffey) と gvs に感謝します。
Dick Swart、Masaki Tagawa、Paul、khcnz、Kolodnerd、およびAlfie Gernerに、元の "mitsubishi_hass" へのコード更新とこのカスタムコンポーネントへの適用に貢献してくれたことに感謝します。
ホームアシスタント用に独自のネイティブ Python ECHONET ライブラリを作成するように促してくれた JeffroCarr に感謝します。
彼自身のリポジトリのいくつかのアイデアは、私自身のコードに実装されました。
(https://github.com/jethrocarr/echonetlite-hvac-mqtt-service.git)
"pychonet"ライブラリの基礎を形成した Node JS の高品質で十分に文書化された ECHONET Lite ライブラリをオープンソーシングしてくれた Futomi Hatano に感謝します。
(https://github.com/futomi/node-echonet-lite)
## ライセンス
このアプリケーションはMITライセンスの下でライセンスされています。詳細については、ライセンスを参照してください。
***
[echonetlite_homeassistant]: https://github.com/scottyphillips/echonetlite_homeassistant
[hacs]: https://github.com/custom-components/hacs
[hacsbadge]: https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge
[releases-shield]: https://img.shields.io/github/release/scottyphillips/echonetlite_homeassistant.svg?style=for-the-badge
[releases]: https://github.com/scottyphillips/echonetlite_homeassistant/releases
[license-shield]:https://img.shields.io/github/license/scottyphillips/echonetlite_homeassistant?style=for-the-badge
[buymecoffee]: https://www.buymeacoffee.com/RgKWqyt?style=for-the-badge
[buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?style=for-the-badge
[maintenance-shield]: https://img.shields.io/badge/Maintainer-Scott%20Phillips-blue?style=for-the-badge
[echonetimg]: ECHONET.jpeg
================================================
FILE: README.md
================================================
# ECHONETLite Platform Custom Component for Home Assistant
[![GitHub Release][releases-shield]][releases]
[![License][license-shield]](LICENSE)
[![hacs][hacsbadge]][hacs]
![Project Maintenance][maintenance-shield]
([日本語](https://github.com/scottyphillips/echonetlite_homeassistant/blob/master/README.ja.md))
A Home Assistant custom component for use with ECHONETLite compatible devices.
This custom component makes use of the 'pychonet'
Python3 library also maintained by this author.
(https://github.com/scottyphillips/pychonet)
*Important note - this repo is no longer in active developement, and i will no longer be actively involved in troubleshooting any issues with the component. However i will respond and approve PRs.
**This component will set up the climate, fan, sensor, select and switch platforms.**
# Current working systems:
Based upon feedback this custom component works with the following
compatible ECHONETLite Devices:
| **Manufacturer** | **Device** | **ECHONETLite Object Class** | **Home Assistant Entities** | **Notes** |
|:--------------------|:-----------------------------------------------|:-----------------------------|:---------------------------------|:--------------------------------------------------------------------------------------------------|
| Mitsubishi Electric | MAC-568IF-E | HomeAirConditioner | Climate, Sensor, Select | WiFi Adaptor connected to various HVAC ducted and split systems. See list below. |
| Mitsubishi Electric | MAC-577IF-E | HomeAirConditioner | Climate, Sensor, Select | WiFi Adaptor connected to various HVAC ducted and split systems. See list below. |
| Mitsubishi Electric | MAC-577IF2-E | HomeAirConditioner | Climate, Sensor, Select | WiFi Adaptor connected to various HVAC ducted and split systems. See list below. |
| Mitsubishi Electric | MAC-578IF2-E | HomeAirConditioner | Climate, Sensor, Select | WiFi Adaptor connected to various HVAC ducted and split systems. See list below. |
| Mitsubishi Electric | MAC-587IF-E | HomeAirConditioner | Climate, Sensor, Select | WiFi Adaptor connected to various HVAC ducted and split systems. See list below. |
| Mitsubishi Electric | MAC-588IF-E | HomeAirConditioner | Climate, Sensor, Select | WiFi Adaptor connected to various HVAC ducted and split systems. See list below. |
| Mitsubishi Electric | MAC-600IF | HomeAirConditioner | Climate, Sensor, Select | |
| Mitsubishi Electric | MAC-900IF | HomeAirConditioner | Climate, Sensor, Select | WiFi Adaptor connected to various HVAC ducted and split systems. See list below. |
| Mitsubishi Electric | HM-W002-AC | HomeAirConditioner | Climate, Sensor, Select | WiFi Adaptor connected to various HVAC ducted and split systems. See list below. |
| Mitsubishi Electric | Eco-Cute SRT-S466A + RMCB-H6SE-T | ElectricWaterHeater | Sensor, Select, Switch | |
| Mitsubishi Electric | REF-WLAN001 | Refrigerator | Sensor | |
| Sharp | AY-J22H Air Conditioner | HomeAirConditioner | Climate, Sensor, Select | |
| Sharp | AY-XP12YHE Air Conditioner | HomeAirConditioner | Climate, Sensor, Select | |
| Sharp | AY-XP24YHE Air Conditioner | HomeAirConditioner | Climate, Sensor, Select | |
| Sharp | AY-L40P Air Conditioner | HomeAirConditioner | Climate, Sensor, Select | |
| Sharp | KI-HS70 Air Purifier | HomeAirCleaner | Fan, Sensor, Select | |
| Sharp | JH-RWL8 Multi Energy Monitor | HomeSolarPower, StorageBattery | Sensor, Select | |
| Panasonic | CS-221DJ Air Conditioner | HomeAirConditioner | Climate, Sensor, Select | |
| Panasonic | CS-362DJ2 Air Conditioner | HomeAirConditioner | Climate, Sensor, Select | |
| Panasonic | HF-JA2-W | | Sensor | IP/JEM-A conversion adapter. |
| Panasonic | Link Plus WTY2001 | GeneralLighting, Lighting system | Light, Select | Lighting system is selector of preset scene. |
| Panasonic | Smart Cosmo Type LAN | DistributionPanelMeter | Sensor | |
| Rinnai | Hot water systems (ECHONETLite enabled models) | | Sensor, Switch, Input | Input entity to configure Hot Water Timers can be configured by using a template and a [Service Call](Services.md). |
| Koizumi | Lighting system AE50264E bridge | LightingSystem | Light, Sensor | https://www.koizumi-lt.co.jp/product/jyutaku/tree/ |
| Daikin | ECHONETLite enabled HVAC models. | HomeAirConditioner | Climate, Sensor, Select | |
| OMRON | Home Solar Power Generation | | Switch, Sensor | Full support for Home Assistant Energy Dashboard including solar production and grid consumption. |
| JDM Electric Meters | Low voltage smart meter (B route service) | | Sensor | Require Wi-SUN <-> Ethernet/Wifi bridge. <br> [nao-pon/python-echonet-lite](https://github.com/nao-pon/python-echonet-lite) |
| Noritz | Bathtub and floor heating system | HotWaterGenerator | Sensor, Switch | |
| KDK | ECHONETLite enabled Ceiling Fans | CeilingFan, GeneralLighting | Fan, Light, Sensor | Rebranded Panasonic Ceiling Fan. |
| Sony | 'MoekadenRoom' ECHONETLite Simulator | | Climate, Select, Switch, Sensor | https://github.com/SonyCSL/MoekadenRoom. |
* Mitsubishi MAC-568IF-E WiFi Adaptor connected to the following systems:
* GE Series
* MSZ-GE42VAD
* MSZ-GE24VAD
* MSZ-GL71VGD
* MSZ-GL50VGD
* MSZ-GL35VGD
* MSZ-GL25VGD
* AP Series
* MSZ-AP22VGD
* MSZ-AP25VGD
* MSZ-AP50VGD
* LN Series
* MSZ-LN25VG2
* MSZ-LN35VG2
* MSZ-LN50VG2
* Ducted
* PEA-M100GAA
* PEA-M100HAA
* PEA-RP140GAA
* Bulkhead
* SEZ-M71DA
* Mitsubishi MAC-578IF2-E WiFi Adaptor connected to the following systems:
* AP Series
* MSZ-AP22VGD
* MSZ-AP35VGD
* MSZ-AP50VGD
* Ducted
* PEAD-RP71
* Mitsubishi MAC-587IF-E WiFi Adaptor connected to the following systems:
* Ducted
* PEAD-M50JA2
* Mitsubishi MAC-588IF-E WiFi Adaptor connected to the following systems:
* Ducted
* PEA-M200LAA
* PEAD-M71JAA
* Mitsubishi MAC-600IF WiFi Adaptor connected to the following systems:
* Z Series
* MSZ-ZW4022S
* Mitsubishi MAC-900IF WiFi Adaptor connected to the following systems:
* Z Series
* MSZ-ZW4024S
* XD Series
* MSZ-XD2225
* R Series
* MSZ-BKR2223
* Mitsubishi HM-W002-AC WiFi Adaptor connected to the following systems:
* JXV Series
* MSZ-JXV4018S
* Mitsubishi REF-WLAN001 WiFi Adaptor connected to the following systems:
* Refrigerator
* MR-WZ55H
* Fujitsu General OP-J03DZ WiFi Adaptor connected to the following systems:
* Air Conditioner
* "Nocria" C Series
* AS-C224R
* AS-C254R
* "Nocria" V Series
* AS-V173N2
## Installation - Enable ECHONET protocol
This Custom Component was originally designed for the Mitsubishi MAC-568IF-E WiFi
Adaptor, a basic guide for enabling ECHONETlite is provided below.
From the official Mitsubishi AU/NZ Wifi App, you will need to enable
the 'ECHONET lite' protocol under the 'edit unit' settings.
![echonet][echonetimg]
Note that the proprietary Mitsubishi app (MELCloud/MELView/Kumo Cloud) controls some models in single ˚F or half ˚C, but
ECHONET works in whole ˚C.
Many other products will work using this custom-component, but they must correctly support the 'ECHONET lite' protocol. The author cannot assist with enabling ECHONET Lite for other vendor products.
### Home Network
If you have a firewall, ensure port 3610 is unblocked
([EchoNet Specifications](https://echonet.jp/spec_v113_lite_en/))
## Installation
### Install using HACS
1. Click the link below or look up 'ECHONETLite Platform' in integrations\
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=scottyphillips&repository=echonetlite_homeassistant&category=integration)
3. Click 'Download', leave the version be and click 'Download' again.
4. Restart Home Assistant
### Install manually
1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`).
2. If you do not have a `custom_components` directory (folder) there, you need to create it.
3. In the `custom_components` directory (folder) create a new folder called `echonetlite`.
4. Download _all_ the files from the `custom_components/echonetlite/` directory (folder) in this repository.
5. Place the files you downloaded in the new directory (folder) you created.
6. Restart Home Assistant and clear your browser cache
## Setup
1. In Home Assistant, go to Settings -> Devices & Services -> ADD INTEGRATION.
2. Select the 'ECHONET Lite' integration. Enter the IP address of the HVAC unit in the host field, and give the platform a name.
3. Platform will automatically configure any supported platforms e.g. climate, sensor, switch, fan, select.
4. If you have additional devices to configure then repeat step 1.
## Enabling support for additional Mitsubishi interfaces
Some Mitsubishi WiFi adaptors have hidden support for the ECHONET Lite protocol, which can be enabled by calling the `/smart` endpoint.
For more information, please see [this issue](https://github.com/scottyphillips/echonetlite_homeassistant/issues/226).
TLDR: Run the following command:
```bash
curl -H 'Content-Type: text/xml' -d '<?xml version="1.0" encoding="UTF-8"?><ESV>7WVvmfhMYzGVi70nyFhmKEy9Jo3Hg3994vi9y1kEgDFWd/1ch9RWDUgY4HgsvMHFvP93fQ30AvEJCNcd0GTwPID0F8V5eyMVj/qAQCXFqYrRtJh8MIpm2/h7jZ2SsPj0</ESV>' "http://${ip}/smart"
```
Replace `${ip}` with the IP of your adaptor.
If successful, your should see a response like this:
```xml
<?xml version="1.0" encoding="UTF-8"?><ESV>[encrypted content]</ESV>
```
## Configuring Options for Fan and swing mode settings for supported HVAC and Air Purifiers.
Once you have added the integration, you can go back to configuration -> integrations.
Under your ECHONETLite device click 'configure'.
Fine tune your required fan and swing mode settings. The integration will be able to determine what settings are supported for your system in question.
NOTE: Selecting which specific options are suitable is a 'trial and error' process as ECHONETLite does not provide a means of returning permittted values for these settings.
As soon as you configure your options and save, the settings will take effect.
## How to find quirks of a manufacturer or specific device
It is known that there are manufacturers or specific devices that have data points that are not in the Echonet Lite specification. If the device you are using has these quirks, it will be more useful to find the quirks and define the behavior. Here are the steps to find the quirks as a first step.
1. Enable the debug log on the ECHONETLite integration screen and restart HA.
2. Once startup is complete, disable the debug log and download the log.
3. Using the IP address of the target device, check the getmap and setmap data to find values above 240 (0xF0).
In the following sample log, 250 (0xFA) and 253 (0xFD) can be get/set. This device is from manufacturer "Rinnai" and has eojgc 2 (0x02) and eojcc 130 (0x82)
```
{'eojcc': 130, 'eojci': 1, 'eojgc': 2, 'getmap': [128, 224, 129, 241, 130, 131, 147, 243, 244, 245, 246, 247, 136, 248, 249, 138, 250, 251, 252, 157, 253, 158, 254, 159, 255], 'host': '192.168.0.49', 'host_product_code': None, 'manufacturer': 'Rinnai', 'name': None, 'ntfmap': [128, 129, 136, 250, 251, 253], 'setmap': [129, 147, 250, 253], 'uid': '0000590170500000000024cd8d4e84f4', 'uidi': '0000590170500000000024cd8d4e84f4-2-130-1'}]}
```
4. Create a quirks file for debugging.
- File name: quirks/Rinnai/all/0282.py (quirks/{manufacturer}/all/xxyy.py)
```python
from homeassistant.const import CONF_NAME
def _hex(edt):
return edt.hex()
QUIRKS = {
0xFA: {
"EPC_FUNCTION": _hex,
"ENL_OP_CODE": {
CONF_NAME: "FA",
},
},
0xFD: {
"EPC_FUNCTION": _hex,
"ENL_OP_CODE": {
CONF_NAME: "FD",
},
},
}
```
5. When you restart the HA, two new entities, FA and FD, will be configured in this example. Perform zone control and observe whether their values change in a distinctive way.
6. If a characteristic change occurs, you are lucky! Please submit a new issue, or go a step further and submit your pull request. 👍
## Hall of Fame
Thanks Naoki Sawada for creating the switch entity, creating the custom service call framework, and a ton of other improvements.
Most importantly of all he contributed the translation into 日本語.
Thanks to scumbug, lordCONAN, and xen2 for contributing some very interesting devices.
Thanks to Jason Nader for all the quality of life updates to the codebase and doco.
Thanks to khcnz (Karl Chaffey) and gvs for helping refector the old code
and contributing to testing.
Thanks to Dick Swart, Masaki Tagawa, Paul, khcnz, Kolodnerd, and Alfie Gerner
for each contributing code updates to to the original 'mitsubishi_hass' and therefore this custom component.
Thanks to Jeffro Carr who inspired me to write my own native Python ECHONET library for Home Assistant.
Some ideas in his own repo got implemented in my own code.
(https://github.com/jethrocarr/echonetlite-hvac-mqtt-service.git)
Thanks to Futomi Hatano for open sourcing a high quality and well documented ECHONET Lite library in Node JS that formed the basis of the 'Pychonet' library.
(https://github.com/futomi/node-echonet-lite)
Thanks to all other contributers who I might have missed for raising PRs and issues which has made this little weekend project into something useful for many people.
## License
This application is licensed under an MIT license, refer to LICENSE for details.
***
[echonetlite_homeassistant]: https://github.com/scottyphillips/echonetlite_homeassistant
[hacs]: https://github.com/custom-components/hacs
[hacsbadge]: https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge
[releases-shield]: https://img.shields.io/github/release/scottyphillips/echonetlite_homeassistant.svg?style=for-the-badge
[releases]: https://github.com/scottyphillips/echonetlite_homeassistant/releases
[license-shield]:https://img.shields.io/github/license/scottyphillips/echonetlite_homeassistant?style=for-the-badge
[buymecoffee]: https://www.buymeacoffee.com/RgKWqyt?style=for-the-badge
[buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?style=for-the-badge
[maintenance-shield]: https://img.shields.io/badge/Maintainer-Scott%20Phillips-blue?style=for-the-badge
[echonetimg]: ECHONET.jpeg
================================================
FILE: Services.ja.md
================================================
# サービスの構成
バージョン 3.5.3 以降、予備的なサポートとして高度なサービス呼び出しが構成されています。 現在、次のデバイスがホームアシスタントサービスをサポートしています。
## ECHONET Lite 対応給湯器 (リンナイ給湯器で動作確認済)
ECHONET Lite 対応給湯器は、`echonetlite.set_on_timer_time`サービスコールを使用して自動湯はりタイマーを設定できます。 これは、以下の例のように自動化で使用して、対応する入力エンティティを作成できます。
自動化のサンプル
- センサーエンティティID: sensor.hot_water_set_value_of_on_timer_time
- 時刻入力エンティティID: input_datetime.hot_water_value_of_on_timer_time
```yaml
alias: Relay Hot Water On Timer time
description: ''
trigger:
- platform: state
entity_id:
- input_datetime.hot_water_value_of_on_timer_time
for:
hours: 0
minutes: 0
seconds: 5
id: set
- platform: state
entity_id:
- sensor.hot_water_set_value_of_on_timer_time
id: get
condition:
- condition: template
value_template: >-
{{strptime(states('input_datetime.hot_water_value_of_on_timer_time'), '%H:%M:%S') !=
strptime(states('sensor.hot_water_set_value_of_on_timer_time')+':00',
'%H:%M:%S')}}
action:
- if:
- condition: trigger
id: set
then:
- service: echonetlite.set_on_timer_time
entity_id: sensor.hot_water_set_value_of_on_timer_time
data_template:
timer_time: '{{ states(''input_datetime.hot_water_value_of_on_timer_time'') }}'
else:
- service: input_datetime.set_datetime
entity_id: input_datetime.hot_water_value_of_on_timer_time
data_template:
time: '{{ states(''sensor.hot_water_set_value_of_on_timer_time'') }}'
mode: single
```
================================================
FILE: Services.md
================================================
# Configuring Services
Preliminary support for advanced service calls has been configured as of version 3.5.3. At the moment the follow devices support Home Assistant services:
## Hot Water Heater System
ECHONET Lite compatible Hot Water Heaters can have their hot water heater timers configured using the `echonetlite.set_on_timer_time` service call. This in turn can be used in automation as per the below example to create a corresponding input entity:
### Automation example
- Sensor entity id as: `sensor.hot_water_set_value_of_on_timer_time`
- Input entity id as: `input_datetime.hot_water_value_of_on_timer_time`
```yaml
alias: Relay Hot Water On Timer time
description: ''
trigger:
- platform: state
entity_id:
- input_datetime.hot_water_value_of_on_timer_time
for:
hours: 0
minutes: 0
seconds: 5
id: set
- platform: state
entity_id:
- sensor.hot_water_set_value_of_on_timer_time
id: get
condition:
- condition: template
value_template: >-
{{strptime(states('input_datetime.hot_water_value_of_on_timer_time'), '%H:%M:%S') !=
strptime(states('sensor.hot_water_set_value_of_on_timer_time')+':00',
'%H:%M:%S')}}
action:
- if:
- condition: trigger
id: set
then:
- service: echonetlite.set_on_timer_time
entity_id: sensor.hot_water_set_value_of_on_timer_time
data_template:
timer_time: '{{ states(''input_datetime.hot_water_value_of_on_timer_time'') }}'
else:
- service: input_datetime.set_datetime
entity_id: input_datetime.hot_water_value_of_on_timer_time
data_template:
time: '{{ states(''sensor.hot_water_set_value_of_on_timer_time'') }}'
mode: single
```
## Single-byte integer value setting services
We can create a numeric setting entity with the following automation with the number input helper.
Of course, it can also be used for various automations.
With future expansion, parameters that can be set numerically on other devices can be added, so I think it can be used conveniently. (Thanks @nao-pon)
- Sensor entity id as: `sensor.set_value_of_hot_water_temperature`
- Input entity id as: `input_number.set_value_of_hot_water_temperature`
### Automation example
```yaml
alias: Relay Set Value Of Hot Water Temperature
description: ''
trigger:
- platform: state
entity_id:
- input_number.set_value_of_hot_water_temperature
for:
hours: 0
minutes: 0
seconds: 2
id: set
- platform: state
entity_id:
- sensor.set_value_of_hot_water_temperature
id: get
condition:
- condition: template
value_template: >-
{{int(states('input_number.set_value_of_hot_water_temperature'), 0) !=
int(states('sensor.set_value_of_hot_water_temperature'), 0)}}
action:
- if:
- condition: trigger
id: set
then:
- service: echonetlite.set_value_int_1b
entity_id: sensor.set_value_of_hot_water_temperature
data_template:
value: '{{ states(''input_number.set_value_of_hot_water_temperature'') }}'
else:
- service: input_number.set_value
entity_id: input_number.set_value_of_hot_water_temperature
data_template:
value: '{{ states(''sensor.set_value_of_hot_water_temperature'') }}'
mode: queued
max: 2
```
================================================
FILE: custom_components/echonetlite/__init__.py
================================================
"""The echonetlite integration."""
from __future__ import annotations
import asyncio
import logging
import os
import time as pytime
from datetime import timedelta
from functools import partial
from importlib import import_module
from typing import Any
import pychonet as echonet
from homeassistant import config_entries
from homeassistant.components.number.const import NumberDeviceClass
from homeassistant.components.sensor.const import SensorDeviceClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_NAME,
PERCENTAGE,
Platform,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfPower,
UnitOfTemperature,
UnitOfVolume,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.util import Throttle
from pychonet import ECHONETAPIClient
from pychonet.echonetapiclient import EchonetMaxOpcError
from pychonet.EchonetInstance import (
ENL_CUMULATIVE_POWER,
ENL_GETMAP,
ENL_INSTANTANEOUS_POWER,
ENL_SETMAP,
ENL_STATUS,
ENL_UID,
)
from pychonet.HomeAirConditioner import (
ENL_AIR_HORZ,
ENL_AIR_VERT,
ENL_AUTO_DIRECTION,
ENL_FANSPEED,
ENL_SWING_MODE,
)
from pychonet.lib.const import ENL_STATMAP, VERSION
from pychonet.lib.epc import EPC_CODE, EPC_SUPER
from pychonet.lib.epc_functions import (
DICT_30_ON_OFF,
DICT_30_OPEN_CLOSED,
DICT_30_TRUE_FALSE,
DICT_41_ON_OFF,
_hh_mm,
)
from pychonet.lib.udpserver import UDPServer
from .config_flow import ErrorConnect, async_discover_newhost, enumerate_instances
from .const import (
CONF_BATCH_SIZE_MAX,
CONF_ENABLE_SUPER_ENERGY,
DOMAIN,
ENABLE_SUPER_ENERGY_DEFAULT,
ENL_OP_CODES,
ENL_SUPER_CODES,
ENL_SUPER_ENERGES,
ENL_TIMER_SETTING,
MISC_OPTIONS,
TEMP_OPTIONS,
USER_OPTIONS,
)
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.SENSOR,
Platform.CLIMATE,
Platform.SELECT,
Platform.LIGHT,
Platform.FAN,
Platform.SWITCH,
Platform.TIME,
Platform.NUMBER,
Platform.COVER,
]
PARALLEL_UPDATES = 0
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1)
MAX_UPDATE_BATCH_SIZE = 10
MIN_UPDATE_BATCH_SIZE = 3
SETUP_BUDGET = 45.0
DISCOVERY_MAX_BUDGET = 20.0
DISCOVERY_MIN_BUDGET = 8.0
INSTANCE_MAX_BUDGET = 8.0
INSTANCE_MIN_BUDGET = 4.0
INSTANCE_RETRY_DELAY = 0.3
def _remaining_setup_budget(started: float) -> float:
"""Return remaining setup budget in seconds."""
return SETUP_BUDGET - (pytime.monotonic() - started)
async def _run_with_timeout(coro, timeout_s: float):
"""Run coroutine with timeout."""
async with asyncio.timeout(timeout_s):
return await coro
def get_device_name(connector, config) -> str:
if connector._name:
return connector._name
if connector._instance._eojci > 1:
return f"{config.title} {connector._instance._eojci}"
return config.title
def get_name_by_epc_code(
jgc: int,
jcc: int,
code: int,
unknown: str | None = None,
given_name: str | None = None,
) -> str:
if given_name != None:
return given_name
if code in EPC_SUPER:
return EPC_SUPER[code]
else:
if unknown == None:
unknown = f"({code})"
name = EPC_CODE.get(jgc, {}).get(jcc, {}).get(code, None)
if name == None:
_code = f"[{hex(jgc)}({jgc})-{hex(jcc)}({jcc})-{hex(code)}({code})]"
_LOGGER.warning(
f"{_code} - Unable to resolve the item name. "
+ f"Please report the unknown code {_code} at the issue tracker on GitHub!"
)
if unknown == None:
name = f"({code})"
else:
name = unknown
return name
def polling_update_debug_log(values: dict[int, Any], conn_instance: ECHONETConnector):
eojgc = conn_instance._eojgc
eojcc = conn_instance._eojcc
debug_log = "\nECHONETlite polling update data:\n"
for value in list(values.keys()):
name = conn_instance._enl_op_codes.get(value, {}).get(CONF_NAME)
debug_log = (
debug_log
+ f" - {get_name_by_epc_code(eojgc, eojcc, value, None, name)} {value:#x}({value}): {values[value]}\n"
)
return debug_log
def get_unit_by_devise_class(device_class: str) -> str | None:
if (
device_class == SensorDeviceClass.TEMPERATURE
or device_class == NumberDeviceClass.TEMPERATURE
):
unit = UnitOfTemperature.CELSIUS
elif (
device_class == SensorDeviceClass.ENERGY
or device_class == NumberDeviceClass.ENERGY
):
unit = UnitOfEnergy.WATT_HOUR
elif (
device_class == SensorDeviceClass.POWER
or device_class == NumberDeviceClass.POWER
):
unit = UnitOfPower.WATT
elif (
device_class == SensorDeviceClass.CURRENT
or device_class == NumberDeviceClass.CURRENT
):
unit = UnitOfElectricCurrent.AMPERE
elif (
device_class == SensorDeviceClass.VOLTAGE
or device_class == NumberDeviceClass.VOLTAGE
):
unit = UnitOfElectricPotential.VOLT
elif (
device_class == SensorDeviceClass.HUMIDITY
or device_class == SensorDeviceClass.BATTERY
or device_class == NumberDeviceClass.HUMIDITY
or device_class == NumberDeviceClass.BATTERY
):
unit = PERCENTAGE
elif device_class == SensorDeviceClass.GAS or device_class == NumberDeviceClass.GAS:
unit = UnitOfVolume.CUBIC_METERS
elif (
device_class == SensorDeviceClass.WATER
or device_class == NumberDeviceClass.WATER
):
unit = UnitOfVolume.CUBIC_METERS
else:
unit = None
return unit
def regist_as_inputs(epc_function_data):
if epc_function_data:
if type(epc_function_data) == list:
if type(epc_function_data[1]) == dict and len(epc_function_data[1]) > 1:
return True # Switch or Select
if callable(epc_function_data[0]) and epc_function_data[0] == _hh_mm:
return True # Time
elif callable(epc_function_data) and epc_function_data == _hh_mm:
return True # Time
return False
def regist_as_binary_sensor(epc_function_data):
if epc_function_data:
if type(epc_function_data) == list:
if epc_function_data[1] in (
DICT_41_ON_OFF,
DICT_30_TRUE_FALSE,
DICT_30_ON_OFF,
DICT_30_OPEN_CLOSED,
):
return True
return False
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.async_on_unload(entry.add_update_listener(update_listener))
started = pytime.monotonic()
host = None
udp = None
server = None
async def discover_callback(host):
await async_discover_newhost(hass, host)
def unload_config_entry():
if server != None:
_LOGGER.debug(
f"Called unload_config_entry() try to remove {host} from server._state."
)
if server._state.get(host):
server._state.pop(host)
# Remove update callback function
for _key in list(server._update_callbacks.keys()):
if _key.startswith(host):
del server._update_callbacks[_key]
entry.async_on_unload(unload_config_entry)
if DOMAIN in hass.data: # maybe set up by config entry?
_LOGGER.debug("ECHONETlite platform is already started.")
server = hass.data[DOMAIN]["api"]
hass.data[DOMAIN].update({entry.entry_id: []})
else: # setup API
_LOGGER.debug("Starting up ECHONETlite platform..")
_LOGGER.debug(f"pychonet version is {VERSION}")
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN].update({entry.entry_id: []})
udp = UDPServer()
udp.run("0.0.0.0", 3610, loop=hass.loop)
server = ECHONETAPIClient(udp)
server._debug_flag = True
server._logger = _LOGGER.debug
server._message_timeout = 300
server._discover_callback = discover_callback
hass.data[DOMAIN].update({"api": server})
if not entry.pref_disable_new_entities:
host = (
entry.data["host"]
if "host" in entry.data
else entry.data["instances"][0]["host"]
)
# make sure multicast is registered with the local IP used to reach this host
server._server.register_multicast_from_host(host)
try:
remaining = _remaining_setup_budget(started)
if remaining < DISCOVERY_MIN_BUDGET:
raise ConfigEntryNotReady(
f"Not enough setup time left for ECHONET Lite discovery on {host}"
)
discovery_budget = min(remaining, DISCOVERY_MAX_BUDGET)
instances = await _run_with_timeout(
enumerate_instances(hass, host),
discovery_budget,
)
except ErrorConnect as ex:
raise ConfigEntryNotReady(
f"Connection error while connecting to {host}: {ex}"
) from ex
except (TimeoutError, asyncio.TimeoutError) as ex:
raise ConfigEntryNotReady(
f"ECHONET Lite discovery timed out for {host}"
) from ex
except asyncio.CancelledError:
raise
# Maintain old entity configuration types to avoid duplicate creation of new entities
_registed_instances = {}
for _instance in entry.data["instances"]:
_uidi = f"{_instance['uid']}-{_instance['eojgc']}-{_instance['eojcc']}-{_instance['eojci']}"
_registed_instances[_uidi] = _instance
for _instance in instances:
_uidi = _instance["uidi"]
_registed = _registed_instances.get(_uidi)
if _registed and _registed.get("uidi") == None:
# keep old type config (echonetlite < 3.6.0)
del _instance["uidi"]
hass.config_entries.async_update_entry(
entry, title=entry.title, data={"host": host, "instances": instances}
)
for instance in entry.data["instances"]:
# auto update to new style
if "ntfmap" not in instance:
instance["ntfmap"] = []
echonetlite = None
host = instance["host"]
eojgc = instance["eojgc"]
eojcc = instance["eojcc"]
eojci = instance["eojci"]
ntfmap = instance["ntfmap"]
getmap = instance["getmap"]
setmap = instance["setmap"]
uid = instance["uid"]
# manually update API states using config entry data
if host not in list(server._state):
server._state[host] = {
"instances": {
eojgc: {
eojcc: {
eojci: {
ENL_STATMAP: ntfmap,
ENL_SETMAP: setmap,
ENL_GETMAP: getmap,
ENL_UID: uid,
}
}
}
}
}
if eojgc not in list(server._state[host]["instances"]):
server._state[host]["instances"].update(
{
eojgc: {
eojcc: {
eojci: {
ENL_STATMAP: ntfmap,
ENL_SETMAP: setmap,
ENL_GETMAP: getmap,
ENL_UID: uid,
}
}
}
}
)
if eojcc not in list(server._state[host]["instances"][eojgc]):
server._state[host]["instances"][eojgc].update(
{
eojcc: {
eojci: {
ENL_STATMAP: ntfmap,
ENL_SETMAP: setmap,
ENL_GETMAP: getmap,
ENL_UID: uid,
}
}
}
)
if eojci not in list(server._state[host]["instances"][eojgc][eojcc]):
server._state[host]["instances"][eojgc][eojcc].update(
{
eojci: {
ENL_STATMAP: ntfmap,
ENL_SETMAP: setmap,
ENL_GETMAP: getmap,
ENL_UID: uid,
}
}
)
echonetlite = ECHONETConnector(instance, hass, entry)
await echonetlite.startup()
try:
# Since there is a small chance of failure, perform a few retries for each instance.
for retry in range(1, 4):
remaining = _remaining_setup_budget(started)
if remaining < INSTANCE_MIN_BUDGET:
raise ConfigEntryNotReady(
f"Not enough setup time left to initialize ECHONET Lite instances for {host}"
)
per_try_budget = min(remaining, INSTANCE_MAX_BUDGET)
try:
await _run_with_timeout(
echonetlite.async_update(),
per_try_budget,
)
hass.data[DOMAIN][entry.entry_id].append(
{"instance": instance, "echonetlite": echonetlite}
)
break
except (TimeoutError, asyncio.TimeoutError) as ex:
_LOGGER.debug(
"Setting up ECHONET instance %s-%s-%s on %s timed out "
"(retry %s/3, remaining %.1fs)",
eojgc,
eojcc,
eojci,
host,
retry,
_remaining_setup_budget(started),
)
if retry == 3:
raise ConfigEntryNotReady(
f"Initial update timed out for {host}"
) from ex
await asyncio.sleep(INSTANCE_RETRY_DELAY)
except asyncio.CancelledError:
raise
except KeyError as ex:
raise ConfigEntryNotReady(
f"IP address change was detected during setup of {host}"
) from ex
_LOGGER.debug(f"Plaform entry data - {entry.data}")
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
# TODO update for Air Cleaner
async def update_listener(hass, entry):
for instance in hass.data[DOMAIN][entry.entry_id]:
if instance["instance"]["eojgc"] == 1 and instance["instance"]["eojcc"] == 48:
for option in USER_OPTIONS.keys():
if (
entry.options.get(USER_OPTIONS[option]["option"]) is not None
): # check if options has been created
if isinstance(
entry.options.get(USER_OPTIONS[option]["option"]), list
):
if (
len(entry.options.get(USER_OPTIONS[option]["option"])) > 0
): # if it has been created then check list length.
instance["echonetlite"]._user_options.update(
{
option: entry.options.get(
USER_OPTIONS[option]["option"]
)
}
)
else:
instance["echonetlite"]._user_options.update(
{option: False}
)
else:
instance["echonetlite"]._user_options.update(
{option: entry.options.get(USER_OPTIONS[option]["option"])}
)
for option in TEMP_OPTIONS.keys():
if entry.options.get(option) is not None:
instance["echonetlite"]._user_options.update(
{option: entry.options.get(option)}
)
for key, option in MISC_OPTIONS.items():
if entry.options.get(key) is not None or option.get("default"):
instance["echonetlite"]._user_options.update(
{key: entry.options.get(key, option.get("default"))}
)
_need_reload = False
for func in instance["echonetlite"]._update_option_func:
_need_reload |= bool(func())
if _need_reload:
return await hass.config_entries.async_reload(entry.entry_id)
else:
return None
async def get_echonet_connector():
return
class ECHONETConnector:
"""EchonetAPIConnector is used to centralise API calls for Echonet devices.
API calls are aggregated per instance (not per node!)"""
def __init__(self, instance, hass, entry):
self.hass = hass
self._host = instance["host"]
self._eojgc = instance["eojgc"]
self._eojcc = instance["eojcc"]
self._eojci = instance["eojci"]
self._update_flag_batches = []
self._update_data = {}
self._api = hass.data[DOMAIN]["api"]
self._update_callbacks = []
self._update_option_func = []
self._update_flags_full_list = []
self._ntfPropertyMap = instance["ntfmap"]
self._getPropertyMap = instance["getmap"]
self._setPropertyMap = instance["setmap"]
self._manufacturer = None
self._host_product_code = None
self._enl_op_codes = ENL_OP_CODES.get(self._eojgc, {}).get(self._eojcc, {})
if "manufacturer" in instance:
self._manufacturer = instance["manufacturer"]
if "host_product_code" in instance:
self._host_product_code = instance["host_product_code"]
self._uid = instance.get("uid")
self._uidi = instance.get("uidi")
self._name = instance.get("name")
self._api.register_async_update_callbacks(
self._host,
self._eojgc,
self._eojcc,
self._eojci,
self.async_update_callback,
)
self._entry = entry
self._instance = echonet.Factory(
self._host, self._api, self._eojgc, self._eojcc, self._eojci
)
async def startup(self):
entry = self._entry
_LOGGER.debug(
f"Starting ECHONETLite {self._instance.__class__.__name__} instance for {self._eojgc}-{self._eojcc}-{self._eojci}, manufacturer: {self._manufacturer}, host_product_code: {self._host_product_code} at {self._host}"
)
# Check Check the definition of quirk
await self._load_quirk()
# TODO this looks messy.
self._user_options = {
ENL_FANSPEED: False,
ENL_AUTO_DIRECTION: False,
ENL_SWING_MODE: False,
ENL_AIR_VERT: False,
ENL_AIR_HORZ: False,
"min_temp_heat": 15,
"max_temp_heat": 35,
"min_temp_cool": 15,
"max_temp_cool": 35,
"min_temp_auto": 15,
"max_temp_auto": 35,
}
# User selectable options for fan + swing modes for HVAC
for option in USER_OPTIONS.keys():
if (
entry.options.get(USER_OPTIONS[option]["option"]) is not None
): # check if options has been created
if (
len(entry.options.get(USER_OPTIONS[option]["option"])) > 0
): # if it has been created then check list length.
self._user_options[option] = entry.options.get(
USER_OPTIONS[option]["option"]
)
# Temperature range options for heat, cool and auto modes
for option in TEMP_OPTIONS.keys():
if entry.options.get(option) is not None:
self._user_options[option] = entry.options.get(option)
# Misc options
for key, option in MISC_OPTIONS.items():
if entry.options.get(key) is not None:
self._user_options[key] = entry.options.get(key, option.get("default"))
# Make _update_flags_full_list
self._make_update_flags_full_list()
self._update_option_func.append(self._make_update_flags_full_list)
# Make batch request flags
self._make_batch_request_flags()
self._update_option_func.append(self._make_batch_request_flags)
_LOGGER.debug(f"UID for ECHONETLite instance at {self._host} is {self._uid}.")
if self._uid is None:
self._uid = f"{self._host}-{self._eojgc}-{self._eojcc}-{self._eojci}"
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self, **kwargs):
try:
await self.async_update_data(kwargs)
except EchonetMaxOpcError as ex:
# Adjust the maximum number of properties for batch requests
batch_size_max = self._user_options.get(
CONF_BATCH_SIZE_MAX, MAX_UPDATE_BATCH_SIZE
)
batch_data_len = max(
ex.args[0],
MIN_UPDATE_BATCH_SIZE,
batch_size_max - 1,
)
if batch_data_len >= batch_size_max:
_LOGGER.error(
f"The integration has adjusted the number of batch requests to {self._host} to {batch_size_max}, but no response is received. Please check and try restarting the device etc."
)
return None
self._user_options[CONF_BATCH_SIZE_MAX] = batch_data_len
entry_options = dict(self._entry.options)
entry_options.update({CONF_BATCH_SIZE_MAX: batch_data_len})
self.hass.config_entries.async_update_entry(
self._entry, options=entry_options
)
self._make_batch_request_flags()
await self.async_update(**kwargs)
async def async_update_data(self, kwargs):
update_data = {}
no_request = "no_request" in kwargs and kwargs["no_request"]
for i, flags in enumerate(self._update_flag_batches):
if i > 0:
# Interval 100ms to next request
await asyncio.sleep(0.1)
batch_data = await self._instance.update(flags, no_request)
if batch_data is not False:
if len(flags) == 1:
update_data[flags[0]] = batch_data
elif isinstance(batch_data, dict):
update_data.update(batch_data)
_LOGGER.debug(polling_update_debug_log(update_data, self))
if len(update_data) > 0:
self._update_data.update(update_data)
async def async_update_callback(self, isPush: bool = False):
await self.async_update_data(kwargs={"no_request": True})
for update_func in self._update_callbacks:
await update_func(isPush)
def _make_update_flags_full_list(self):
_prev_update_flags_full_list = self._update_flags_full_list.copy()
# Make EPC codes for update
self._update_flags_full_list = []
# Is enabled CONF_ENABLE_SUPER_ENERGY
_enabled_super_energy = self._user_options.get(
CONF_ENABLE_SUPER_ENERGY,
ENABLE_SUPER_ENERGY_DEFAULT.get(self._eojgc, {}).get(self._eojcc, True),
)
# General purpose data items
flags = [] # PR 246
if _enabled_super_energy:
_enl_super_codes = ENL_SUPER_CODES
else:
_enl_super_codes = {
k: v for k, v in ENL_SUPER_CODES.items() if k not in ENL_SUPER_ENERGES
}
flags += list(_enl_super_codes)
# Get supported EPC_FUNCTIONS in pychonet object class
_epc_keys = set(self._instance.EPC_FUNCTIONS.keys()) - set(EPC_SUPER.keys())
for item in self._getPropertyMap:
if item in _epc_keys:
flags.append(item)
for value in flags:
if value in self._getPropertyMap:
self._update_flags_full_list.append(value)
self._update_data[value] = None
_LOGGER.debug(
f"Echonet device {self._host}-{self._eojgc}-{self._eojcc}-{self._eojci} update_flags_full_list: {self._update_flags_full_list}"
)
return _prev_update_flags_full_list != self._update_flags_full_list
def _make_batch_request_flags(self):
# Split list of codes into batches of 10
self._update_flag_batches = []
start_index = 0
full_list_length = len(self._update_flags_full_list)
# Make batch request flags
batch_size_max = self._user_options.get(
CONF_BATCH_SIZE_MAX, MAX_UPDATE_BATCH_SIZE
)
while start_index + batch_size_max < full_list_length:
self._update_flag_batches.append(
self._update_flags_full_list[start_index : start_index + batch_size_max]
)
start_index += batch_size_max
self._update_flag_batches.append(
self._update_flags_full_list[start_index:full_list_length]
)
_LOGGER.debug(
f"Echonet device {self._host}-{self._eojgc}-{self._eojcc}-{self._eojci} batch request flags list: {self._update_flag_batches}"
)
def register_async_update_callbacks(self, update_func):
self._update_callbacks.append(update_func)
def add_update_option_listener(self, update_func):
self._update_option_func.append(update_func)
async def _load_quirk(self):
# self._manufacturer, self._host_product_code, self._eojgc, self._eojcc
def update(extention):
for epc in extention.QUIRKS:
if func := extention.QUIRKS[epc].get("EPC_FUNCTION"):
self._instance.EPC_FUNCTIONS.update({epc: func})
if op_code := extention.QUIRKS[epc].get("ENL_OP_CODE"):
self._enl_op_codes.update({epc: op_code})
_LOGGER.debug(f"Echonet EPC_FUNCTIONS is: {self._instance.EPC_FUNCTIONS}")
_LOGGER.debug(f"Echonet _enl_op_codes is: {self._enl_op_codes}")
if self._manufacturer:
check = [
"quirks",
self._manufacturer,
"all",
"{:0>2X}".format(self._eojgc) + "{:0>2X}".format(self._eojcc),
]
path = os.path.dirname(__file__) + "/" + "/".join(check) + ".py"
_LOGGER.debug(f"Echonet _load_quirk check path is: {path}")
if os.path.isfile(path):
mod = "." + ".".join(check)
_LOGGER.debug(f"Echonet import module is: {mod} of {__package__}")
extention = await self.hass.async_add_executor_job(
partial(import_module, mod, package=__package__)
)
update(extention)
if self._host_product_code:
check = [
"quirks",
self._manufacturer,
self._host_product_code,
"{:0>2X}".format(self._eojgc) + "{:0>2X}".format(self._eojcc),
]
path = os.path.dirname(__file__) + "/" + "/".join(check) + ".py"
_LOGGER.debug(f"Echonet _load_quirk check path is: {path}")
if os.path.isfile(path):
mod = "." + ".".join(check)
_LOGGER.debug(f"Echonet import module is: {mod} of {__package__}")
extention = await self.hass.async_add_executor_job(
partial(import_module, mod, package=__package__)
)
update(extention)
================================================
FILE: custom_components/echonetlite/binary_sensor.py
================================================
"""Support for ECHONETLite sensors."""
import logging
import voluptuous as vol
from homeassistant.const import (
CONF_ICON,
CONF_NAME,
CONF_SERVICE,
CONF_TYPE,
CONF_UNIT_OF_MEASUREMENT,
)
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.exceptions import InvalidStateError, NoEntitySpecifiedError
from pychonet.lib.eojx import EOJX_CLASS
from pychonet.lib.epc_functions import (
DATA_STATE_OFF,
DATA_STATE_ON,
DATA_STATE_CLOSE,
DATA_STATE_OPEN,
EPC_SUPER_FUNCTIONS,
)
from . import (
get_name_by_epc_code,
get_unit_by_devise_class,
get_device_name,
regist_as_inputs,
regist_as_binary_sensor,
)
from .const import (
DOMAIN,
ENABLE_SUPER_ENERGY_DEFAULT,
ENL_OP_CODES,
CONF_STATE_CLASS,
ENL_SUPER_CODES,
ENL_SUPER_ENERGES,
NON_SETUP_SINGLE_ENYITY,
TYPE_SWITCH,
TYPE_SELECT,
TYPE_TIME,
TYPE_NUMBER,
SERVICE_SET_ON_TIMER_TIME,
SERVICE_SET_INT_1B,
CONF_FORCE_POLLING,
CONF_ENABLE_SUPER_ENERGY,
TYPE_DATA_DICT,
TYPE_DATA_ARRAY_WITH_SIZE_OPCODE,
CONF_DISABLED_DEFAULT,
CONF_MULTIPLIER,
CONF_MULTIPLIER_OPCODE,
CONF_MULTIPLIER_OPTIONAL_OPCODE,
CONF_ICON_POSITIVE,
CONF_ICON_NEGATIVE,
CONF_ICON_ZERO,
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config, async_add_entities, discovery_info=None):
entities = []
platform = entity_platform.async_get_current_platform()
for entity in hass.data[DOMAIN][config.entry_id]:
_LOGGER.debug(f"Configuring ECHONETLite sensor {entity}")
_LOGGER.debug(
f"Update flags for this sensor are {entity['echonetlite']._update_flags_full_list}"
)
eojgc = entity["instance"]["eojgc"]
eojcc = entity["instance"]["eojcc"]
if entity["echonetlite"]._user_options.get(
CONF_ENABLE_SUPER_ENERGY,
ENABLE_SUPER_ENERGY_DEFAULT.get(eojgc, {}).get(eojcc, True),
):
_enl_super_codes = ENL_SUPER_CODES
else:
_enl_super_codes = {
k: v for k, v in ENL_SUPER_CODES.items() if not k in ENL_SUPER_ENERGES
}
_enl_op_codes = entity["echonetlite"]._enl_op_codes | _enl_super_codes
_epc_functions = (
entity["echonetlite"]._instance.EPC_FUNCTIONS | EPC_SUPER_FUNCTIONS
)
# For all other devices, sensors will be configured but customise if applicable.
for op_code in list(
set(entity["echonetlite"]._update_flags_full_list)
- NON_SETUP_SINGLE_ENYITY.get(eojgc, {}).get(eojcc, set())
):
# Check DeviceClass and regist_as_binary_sensor()
if not isinstance(
_enl_op_codes.get(op_code, {}).get(CONF_TYPE), BinarySensorDeviceClass
) and not regist_as_binary_sensor(_epc_functions.get(op_code, None)):
continue
# Is settable
_is_settable = op_code in entity["instance"]["setmap"]
# Conf Keys list
_keys = _enl_op_codes.get(op_code, {}).keys()
# For backward compatibility (Deprecated)
_has_conf_service = CONF_SERVICE in _keys
# Check this op_code will be configured as input(switch, select ot time) entity
if (
_is_settable
and not _has_conf_service
and regist_as_inputs(_epc_functions.get(op_code, None))
):
continue
# Configuration check with ENL_OP_CODE definition
if len(_keys):
if (
_is_settable
and not _has_conf_service
and (
TYPE_SWITCH in _keys
or TYPE_SELECT in _keys
or TYPE_TIME in _keys
or TYPE_NUMBER in _keys
)
):
continue # dont configure as sensor, it will be configured as switch, select or time instead.
# For backward compatibility (Deprecated)
if (
_is_settable and _has_conf_service
): # Some devices support advanced service calls.
_enl_op_codes[op_code][CONF_DISABLED_DEFAULT] = True
for service_name in _enl_op_codes.get(op_code, {}).get(
CONF_SERVICE
):
if service_name == SERVICE_SET_ON_TIMER_TIME:
platform.async_register_entity_service(
service_name,
{vol.Required("timer_time"): cv.time_period},
"async_" + service_name,
)
elif service_name == SERVICE_SET_INT_1B:
platform.async_register_entity_service(
service_name,
{
vol.Required("value"): cv.positive_int,
vol.Optional(
"epc", default=op_code
): cv.positive_int,
},
"async_" + service_name,
)
if TYPE_DATA_DICT in _keys:
type_data = _enl_op_codes.get(op_code, {}).get(TYPE_DATA_DICT)
if isinstance(type_data, list):
for attr_key in type_data:
entities.append(
EchonetBinarySensor(
entity["echonetlite"],
config,
op_code,
_enl_op_codes.get(op_code) | {"dict_key": attr_key},
hass,
)
)
continue
else:
continue
if TYPE_DATA_ARRAY_WITH_SIZE_OPCODE in _keys:
array_size_op_code = _enl_op_codes[op_code][
TYPE_DATA_ARRAY_WITH_SIZE_OPCODE
]
array_max_size = await entity["echonetlite"]._instance.update(
array_size_op_code
)
for x in range(0, array_max_size):
attr = _enl_op_codes[op_code].copy()
attr["accessor_index"] = x
attr["accessor_lambda"] = lambda value, index: (
value["values"][index] if index < value["range"] else None
)
entities.append(
EchonetBinarySensor(
entity["echonetlite"],
config,
op_code,
attr,
)
)
continue
else:
entities.append(
EchonetBinarySensor(
entity["echonetlite"],
config,
op_code,
_enl_op_codes.get(
op_code,
ENL_OP_CODES["default"] | {CONF_DISABLED_DEFAULT: True},
),
hass,
)
)
continue
entities.append(
EchonetBinarySensor(
entity["echonetlite"],
config,
op_code,
ENL_OP_CODES["default"],
hass,
)
)
async_add_entities(entities, True)
class EchonetBinarySensor(BinarySensorEntity):
"""Representation of an ECHONETLite Temperature Sensor."""
_attr_translation_key = DOMAIN
def __init__(self, connector, config, op_code, attributes, hass=None) -> None:
"""Initialize the sensor."""
name = get_device_name(connector, config)
self._connector = connector
self._op_code = op_code
self._sensor_attributes = attributes
self._eojgc = self._connector._eojgc
self._eojcc = self._connector._eojcc
self._eojci = self._connector._eojci
self._attr_unique_id = (
f"{self._connector._uidi}-{self._op_code}"
if self._connector._uidi
else f"{self._connector._uid}-{self._eojgc}-{self._eojcc}-{self._eojci}-{self._op_code}"
)
self._device_name = name
self._state_value = None
self._server_state = self._connector._api._state[
self._connector._instance._host
]
self._hass = hass
_attr_keys = self._sensor_attributes.keys()
self._attr_icon = self._sensor_attributes.get(CONF_ICON)
self._attr_device_class = self._sensor_attributes.get(CONF_TYPE)
self._attr_state_class = self._sensor_attributes.get(CONF_STATE_CLASS)
# Create name based on sensor description from EPC codes, super class codes or fallback to using the sensor type
self._attr_name = f"{name} {get_name_by_epc_code(self._eojgc, self._eojcc, self._op_code, self._attr_device_class, self._connector._enl_op_codes.get(self._op_code, {}).get(CONF_NAME))}"
if "dict_key" in _attr_keys:
self._attr_unique_id += f'-{self._sensor_attributes["dict_key"]}'
if type(self._sensor_attributes[TYPE_DATA_DICT]) == int:
# As of Version 3.8.0, no configuration is defined that uses this definition.
self._attr_name += f' {str(self._sensor_attributes["accessor_index"] + 1).zfill(len(str(self._sensor_attributes[TYPE_DATA_DICT])))}'
else:
self._attr_name += f' {self._sensor_attributes["dict_key"]}'
if "accessor_index" in _attr_keys:
self._attr_unique_id += f'-{self._sensor_attributes["accessor_index"]}'
self._attr_name += f' {str(self._sensor_attributes["accessor_index"] + 1).zfill(len(str(self._sensor_attributes[TYPE_DATA_ARRAY_WITH_SIZE_OPCODE])))}'
self._attr_entity_registry_enabled_default = not bool(
self._sensor_attributes.get(CONF_DISABLED_DEFAULT)
)
self._attr_should_poll = True
self._attr_available = True
self.update_option_listener()
@property
def device_info(self):
return {
"identifiers": {
(
DOMAIN,
self._connector._uid,
self._connector._eojgc,
self._connector._eojcc,
self._connector._eojci,
)
},
"name": self._device_name,
"manufacturer": self._connector._manufacturer
+ (
" " + self._connector._host_product_code
if self._connector._host_product_code
else ""
),
"model": EOJX_CLASS[self._eojgc][self._eojcc],
# "sw_version": "",
}
def get_attr_is_on(self):
"""Return the state of the sensor."""
if self._op_code in self._connector._update_data:
new_val = self._connector._update_data[self._op_code]
if "dict_key" in self._sensor_attributes:
if hasattr(new_val, "get"):
self._state_value = new_val.get(self._sensor_attributes["dict_key"])
else:
self._state_value = None
elif "accessor_lambda" in self._sensor_attributes:
self._state_value = self._sensor_attributes["accessor_lambda"](
new_val, self._sensor_attributes["accessor_index"]
)
else:
self._state_value = new_val
if self._state_value is None:
return None
_results = {
True: True,
"1": True,
DATA_STATE_ON: True,
DATA_STATE_OPEN: True,
"yes": True,
False: False,
"0": False,
DATA_STATE_OFF: False,
DATA_STATE_CLOSE: False,
"no": False,
}
return _results.get(self._state_value)
async def async_update(self):
"""Retrieve latest state."""
try:
await self._connector.async_update()
self._attr_is_on = self.get_attr_is_on()
except TimeoutError:
pass
async def async_added_to_hass(self):
"""Register callbacks."""
self._connector.add_update_option_listener(self.update_option_listener)
self._connector.register_async_update_callbacks(self.async_update_callback)
async def async_update_callback(self, isPush: bool = False):
new_val = self._connector._update_data.get(self._op_code)
if "dict_key" in self._sensor_attributes:
if hasattr(new_val, "get"):
new_val = new_val.get(self._sensor_attributes["dict_key"])
else:
new_val = None
if "accessor_lambda" in self._sensor_attributes:
new_val = self._sensor_attributes["accessor_lambda"](
new_val, self._sensor_attributes["accessor_index"]
)
changed = (
new_val is not None and self._state_value != new_val
) or self._attr_available != self._server_state["available"]
if changed:
_force = bool(not self._attr_available and self._server_state["available"])
self._state_value = new_val
self._attr_is_on = self.get_attr_is_on()
if self._attr_available != self._server_state["available"]:
if self._server_state["available"]:
self.update_option_listener()
else:
self._attr_should_poll = True
self._attr_available = self._server_state["available"]
self.async_schedule_update_ha_state(_force)
def update_option_listener(self):
_should_poll = self._op_code not in self._connector._ntfPropertyMap
self._attr_should_poll = (
self._connector._user_options.get(CONF_FORCE_POLLING, False) or _should_poll
)
self._attr_extra_state_attributes = {"notify": "No" if _should_poll else "Yes"}
_LOGGER.debug(
f"{self._attr_name}({self._op_code}): _should_poll is {_should_poll}"
)
================================================
FILE: custom_components/echonetlite/climate.py
================================================
import logging
import math
import voluptuous as vol
from homeassistant.components.climate import (
ClimateEntity,
)
from homeassistant.components.climate.const import (
ATTR_HVAC_MODE,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.const import (
ATTR_TEMPERATURE,
PRECISION_WHOLE,
UnitOfTemperature,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import entity_platform
from pychonet.HomeAirConditioner import (
AIRFLOW_VERT,
ENL_AIR_VERT,
ENL_AUTO_DIRECTION,
ENL_FANSPEED,
ENL_HVAC_MODE,
ENL_HVAC_ROOM_TEMP,
ENL_HVAC_SET_HUMIDITY,
ENL_HVAC_SET_TEMP,
ENL_HVAC_SILENT_MODE,
ENL_STATUS,
ENL_SWING_MODE,
FAN_SPEED,
SILENT_MODE,
)
from pychonet.lib.eojx import EOJX_CLASS
from . import get_device_name
from .const import DATA_STATE_ON, DOMAIN, OPTION_HA_UI_SWING
_LOGGER = logging.getLogger(__name__)
DEFAULT_FAN_MODES = list(
FAN_SPEED.keys()
) # ["auto","minimum","low","medium-low","medium","medium-high","high","very-high","max"]
DEFAULT_HVAC_MODES = [
HVACMode.HEAT,
HVACMode.COOL,
HVACMode.DRY,
HVACMode.FAN_ONLY,
HVACMode.HEAT_COOL,
HVACMode.OFF,
]
DEFAULT_SWING_MODES = ["auto-vert"] + list(
AIRFLOW_VERT.keys()
) # ["auto-vert","upper","upper-central","central","lower-central","lower"]
DEFAULT_PRESET_MODES = list(SILENT_MODE.keys()) # ["normal", "high-speed", "silent"]
SERVICE_SET_HUMIDIFER_DURING_HEATER = "set_humidifier_during_heater"
ATTR_STATE = "state"
ATTR_HUMIDITY = "humidity"
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up entry."""
entities = []
for entity in hass.data[DOMAIN][config_entry.entry_id]:
if (
entity["instance"]["eojgc"] == 0x01 and entity["instance"]["eojcc"] == 0x30
): # Home Air Conditioner
entities.append(EchonetClimate(entity["echonetlite"], config_entry))
async_add_devices(entities, True)
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
SERVICE_SET_HUMIDIFER_DURING_HEATER,
{
vol.Required(ATTR_STATE): cv.boolean,
vol.Required(ATTR_HUMIDITY): cv.byte,
},
"async_set_humidifier_during_heater",
)
class EchonetClimate(ClimateEntity):
"""Representation of an ECHONETLite climate device."""
_attr_translation_key = DOMAIN
def __init__(self, connector, config):
"""Initialize the climate device."""
name = get_device_name(connector, config)
self._attr_name = name
self._device_name = name
self._connector = connector # new line
self._attr_unique_id = (
self._connector._uidi if self._connector._uidi else self._connector._uid
)
# The temperature unit of echonet lite is defined as Celsius.
# Set temperature_unit setting to Celsius,
# HA's automatic temperature unit conversion function works correctly.
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_precision = PRECISION_WHOLE
self._attr_target_temperature_step = 1
if hasattr(ClimateEntityFeature, "TURN_ON"):
self._attr_supported_features = ClimateEntityFeature(
ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
)
else:
self._attr_supported_features = ClimateEntityFeature(0)
self._attr_supported_features = (
self._attr_supported_features | ClimateEntityFeature.TARGET_TEMPERATURE
)
self._server_state = self._connector._api._state[
self._connector._instance._host
]
self._opc_data = {
ENL_AUTO_DIRECTION: list(
self._connector._instance.EPC_FUNCTIONS[ENL_AUTO_DIRECTION][1].values()
),
ENL_SWING_MODE: list(
self._connector._instance.EPC_FUNCTIONS[ENL_SWING_MODE][1].values()
),
}
if ENL_FANSPEED in list(self._connector._setPropertyMap):
self._attr_supported_features = (
self._attr_supported_features | ClimateEntityFeature.FAN_MODE
)
if ENL_AIR_VERT in list(
self._connector._setPropertyMap
) or ENL_SWING_MODE in list(self._connector._setPropertyMap):
self._attr_supported_features = (
self._attr_supported_features | ClimateEntityFeature.SWING_MODE
)
if ENL_HVAC_SILENT_MODE in list(self._connector._setPropertyMap):
self._attr_supported_features = (
self._attr_supported_features | ClimateEntityFeature.PRESET_MODE
)
self._attr_hvac_modes = DEFAULT_HVAC_MODES
self._attr_preset_modes = DEFAULT_PRESET_MODES
self._olddata = {}
self._last_mode = HVACMode.OFF
self._attr_should_poll = True
self._attr_available = True
self.update_option_listener()
self._set_attrs()
# see, https://developers.home-assistant.io/blog/2024/01/24/climate-climateentityfeatures-expanded
self._enable_turn_on_off_backwards_compatibility = False
async def async_update(self):
"""Get the latest state from the HVAC."""
try:
await self._connector.async_update()
except TimeoutError:
pass
@property
def device_info(self):
return {
"identifiers": {
(
DOMAIN,
self._connector._uid,
self._connector._instance._eojgc,
self._connector._instance._eojcc,
self._connector._instance._eojci,
)
},
"name": self._device_name,
"manufacturer": self._connector._manufacturer
+ (
" " + self._connector._host_product_code
if self._connector._host_product_code
else ""
),
"model": EOJX_CLASS[self._connector._instance._eojgc][
self._connector._instance._eojcc
],
# "sw_version": "",
}
def _set_min_max_temp(self):
self._attr_min_temp = self._connector._user_options["min_temp_auto"]
self._attr_max_temp = self._connector._user_options["max_temp_auto"]
if hasattr(self, "_attr_hvac_mode"):
"""minimum/maximum temperature supported by the HVAC."""
if self._attr_hvac_mode == HVACMode.HEAT:
self._attr_min_temp = self._connector._user_options["min_temp_heat"]
self._attr_max_temp = self._connector._user_options["max_temp_heat"]
elif self._attr_hvac_mode == HVACMode.COOL:
self._attr_min_temp = self._connector._user_options["min_temp_cool"]
self._attr_max_temp = self._connector._user_options["max_temp_cool"]
def _set_attrs(self):
"""current temperature."""
_val = self._connector._update_data.get(ENL_HVAC_ROOM_TEMP)
# 0x7F: Overflow, 0x80: Underflow, 0x7E:Value cannot be returned
if _val in {0x7F, 0x80, 0x7E}:
_val = None
self._attr_current_temperature = _val
"""temperature we try to reach."""
_val = self._connector._update_data.get(ENL_HVAC_SET_TEMP)
# -3: Rule of thumb, 0xFD: Temperature indeterminable
if _val in {-3, 0xFD}:
_val = None
self._attr_target_temperature = _val
"""temperature we try to reach."""
self._attr_target_humidity = self._connector._update_data.get(
ENL_HVAC_SET_HUMIDITY
)
"""current operation ie. heat, cool, idle."""
_val = self._connector._update_data.get(ENL_HVAC_MODE)
self._attr_hvac_mode = HVACMode.OFF
if self._connector._update_data[ENL_STATUS] == DATA_STATE_ON:
if _val == "auto":
self._attr_hvac_mode = HVACMode.HEAT_COOL
elif _val == "other":
if self._connector._user_options.get(ENL_HVAC_MODE) == "as_idle":
self._attr_hvac_mode = self._last_mode
else:
self._attr_hvac_mode = HVACMode.OFF
else:
self._attr_hvac_mode = _val
if self._attr_hvac_mode != HVACMode.OFF:
self._last_mode = self._attr_hvac_mode
"""current operation ie. heat, cool, idle."""
self._attr_hvac_action = HVACAction.OFF
if self._connector._update_data[ENL_STATUS] == DATA_STATE_ON:
if self._connector._update_data[ENL_HVAC_MODE] == HVACMode.HEAT:
self._attr_hvac_action = HVACAction.HEATING
elif self._connector._update_data[ENL_HVAC_MODE] == HVACMode.COOL:
self._attr_hvac_action = HVACAction.COOLING
elif self._connector._update_data[ENL_HVAC_MODE] == HVACMode.DRY:
self._attr_hvac_action = HVACAction.DRYING
elif self._connector._update_data[ENL_HVAC_MODE] == HVACMode.FAN_ONLY:
self._attr_hvac_action = HVACAction.FAN
elif (
self._connector._update_data[ENL_HVAC_MODE] == HVACMode.HEAT_COOL
or self._connector._update_data[ENL_HVAC_MODE] == "auto"
):
_room_temp = self._connector._update_data.get(ENL_HVAC_ROOM_TEMP)
if _room_temp := self._connector._update_data.get(ENL_HVAC_ROOM_TEMP):
if self._connector._update_data[ENL_HVAC_SET_TEMP] < _room_temp:
self._attr_hvac_action = HVACAction.COOLING
elif self._connector._update_data[ENL_HVAC_SET_TEMP] > _room_temp:
self._attr_hvac_action = HVACAction.HEATING
else:
self._attr_hvac_action = HVACAction.IDLE
elif self._connector._update_data[ENL_HVAC_MODE] == "other":
if self._connector._user_options.get(ENL_HVAC_MODE) == "as_idle":
self._attr_hvac_action = HVACAction.IDLE
else:
self._attr_hvac_action = HVACAction.OFF
else:
_LOGGER.warning(
f"Unknown HVAC mode {self._connector._update_data.get(ENL_HVAC_MODE)}"
)
self._attr_hvac_action = HVACAction.IDLE
"""true if the device is on."""
self._attr_is_on = (
True if self._connector._update_data[ENL_STATUS] == DATA_STATE_ON else False
)
"""fan setting."""
self._attr_fan_mode = (
self._connector._update_data[ENL_FANSPEED]
if ENL_FANSPEED in self._connector._update_data
else None
)
"""preset setting."""
self._attr_preset_mode = (
self._connector._update_data[ENL_HVAC_SILENT_MODE]
if ENL_HVAC_SILENT_MODE in self._connector._update_data
else None
)
"""swing mode setting."""
if (
self._connector._update_data.get(ENL_AUTO_DIRECTION)
in self._attr_swing_modes
):
self._attr_swing_mode = self._connector._update_data.get(ENL_AUTO_DIRECTION)
elif self._connector._update_data.get(ENL_SWING_MODE) in self._attr_swing_modes:
self._attr_swing_mode = self._connector._update_data.get(ENL_SWING_MODE)
else:
self._attr_swing_mode = (
self._connector._update_data[ENL_AIR_VERT]
if ENL_AIR_VERT in self._connector._update_data
else None
)
self._set_min_max_temp()
async def async_set_fan_mode(self, fan_mode):
"""Set new fan mode."""
_LOGGER.debug(f"Updated fan mode is: {fan_mode}")
await self._connector._instance.setFanSpeed(fan_mode)
async def async_set_preset_mode(self, preset_mode):
"""Set new preset mode - This is normal/high-speed/silent"""
await self._connector._instance.setSilentMode(preset_mode)
async def async_set_swing_mode(self, swing_mode):
"""Set new swing mode."""
if swing_mode in self._opc_data[ENL_AUTO_DIRECTION]:
await self._connector._instance.setAutoDirection(swing_mode)
elif swing_mode in self._opc_data[ENL_SWING_MODE]:
await self._connector._instance.setSwingMode(swing_mode)
else:
await self._connector._instance.setAirflowVert(swing_mode)
async def async_set_temperature(self, **kwargs):
"""Set new target temperatures."""
# Check has HVAC Mode
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
if hvac_mode is not None:
await self.async_set_hvac_mode(hvac_mode)
settemp = self._normalize_settemp(kwargs.get(ATTR_TEMPERATURE))
if kwargs.get(ATTR_TEMPERATURE) is not None:
await self._connector._instance.setOperationalTemperature(settemp)
async def async_set_humidity(self, humidity: int) -> None:
await self._connector._instance.setOperationalTemperature(humidity)
async def async_set_hvac_mode(self, hvac_mode):
# _LOGGER.warning(self._connector._update_data)
"""Set new operation mode (including off)"""
if hvac_mode == "heat_cool":
await self._connector._instance.setMode("auto")
else:
await self._connector._instance.setMode(hvac_mode)
async def async_turn_on(self):
"""Turn on."""
await self._connector._instance.on()
async def async_turn_off(self):
"""Turn off."""
await self._connector._instance.off()
async def async_set_humidifier_during_heater(self, state, humidity):
"""Handle boost heating service call."""
await self._connector._instance.setHeaterHumidifier(state, humidity)
async def async_added_to_hass(self):
"""Register callbacks."""
self._connector.add_update_option_listener(self.update_option_listener)
self._connector.register_async_update_callbacks(self.async_update_callback)
async def async_update_callback(self, isPush: bool = False):
changed = (
self._olddata != self._connector._update_data
or self._attr_available != self._server_state["available"]
)
_LOGGER.debug(
f"Called async_update_callback on {self._device_name}.\nChanged: {changed}\nUpdate data: {self._connector._update_data}\nOld data: {self._olddata}"
)
if changed:
_force = bool(not self._attr_available and self._server_state["available"])
self._olddata = self._connector._update_data.copy()
self._attr_available = self._server_state["available"]
self._set_attrs()
self.async_schedule_update_ha_state(_force | isPush)
def update_option_listener(self):
"""list of available fan modes."""
_modes = self._connector._user_options.get(ENL_FANSPEED)
if _modes:
self._attr_fan_modes = _modes
else:
self._attr_fan_modes = DEFAULT_FAN_MODES
"""list of available swing modes."""
_modes = self._connector._user_options.get(OPTION_HA_UI_SWING)
if _modes and len(_modes):
self._attr_swing_modes = _modes
elif _modes := self._connector._user_options.get(ENL_AIR_VERT):
self._attr_swing_modes = _modes
else:
self._attr_swing_modes = DEFAULT_SWING_MODES
self._set_min_max_temp()
if self.hass:
self.async_schedule_update_ha_state()
def _normalize_settemp(self, req: float | int | None) -> int | None:
"""
Normalize a requested temperature to the 1°C resolution supported by
ECHONET Lite HVAC devices.
Matter controllers may send fractional values (e.g., 22.5°C). Since most
ECHONET air conditioners accept only integer setpoints, this function
converts the request to a valid value while preserving user intent:
- Integer values are used as-is.
- `.5` values are rounded directionally based on the previous target
temperature (up when increasing, down when decreasing).
- Other fractions are rounded to the nearest integer.
"""
if req is None:
return None
res = None
if abs(req - round(req)) < 1e-9:
res = int(round(req))
else:
prev = self._attr_target_temperature
frac = req - math.floor(req)
if abs(frac - 0.5) < 1e-9 and prev is not None:
if req >= prev:
res = math.ceil(req)
if req < prev:
res = math.floor(req)
else:
res = int(math.floor(req + 0.5))
return res
================================================
FILE: custom_components/echonetlite/config_flow.py
================================================
"""Config flow for echonetlite integration."""
from __future__ import annotations
import logging
import asyncio
from typing import Any
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntryState, ConfigFlowResult
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.selector import selector
from pychonet.lib.const import (
ENL_STATMAP,
ENL_SETMAP,
ENL_GETMAP,
GET,
)
# from aioudp import UDPServer
from pychonet.lib.udpserver import UDPServer
from pychonet.lib.epc_functions import _null_padded_optional_string
# from pychonet import Factory
from pychonet import ECHONETAPIClient
from pychonet.HomeAirConditioner import (
ENL_AIR_VERT,
ENL_AUTO_DIRECTION,
ENL_SWING_MODE,
)
from .const import (
DOMAIN,
USER_OPTIONS,
TEMP_OPTIONS,
MISC_OPTIONS,
ENL_HVAC_MODE,
CONF_OTHER_MODE,
OPTION_HA_UI_SWING,
)
_LOGGER = logging.getLogger(__name__)
WORD_OF_AUTO_DISCOVERY = "[Auto Discovery]"
# TODO adjust the data schema to the data that you need
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required("host", default=WORD_OF_AUTO_DISCOVERY): str,
vol.Required("title", default=WORD_OF_AUTO_DISCOVERY): str,
}
)
_detected_hosts = {}
_init_server = None
async def enumerate_instances(
hass: HomeAssistant, host: str, newhost: bool = False
) -> list[dict[str, Any]]:
"""Validate the user input allows us to connect."""
_LOGGER.debug(f"IP address is {host}")
server = None
if DOMAIN in hass.data: # maybe set up by config entry?
_LOGGER.debug("API listener has already been setup previously..")
server = hass.data[DOMAIN]["api"]
for key in hass.data[DOMAIN]:
if key != "api":
entries = hass.data[DOMAIN][key]
if len(entries):
inst = entries[0].get("instance")
if inst:
if inst.get("host") == host:
raise ErrorConnect("already_configured")
elif _init_server:
_LOGGER.debug("API listener has already been setup in init_discover()")
server = _init_server
else:
udp = UDPServer()
udp.run("0.0.0.0", 3610, loop=hass.loop)
server = ECHONETAPIClient(server=udp)
server._debug_flag = True
server._logger = _LOGGER.debug
server._message_timeout = 300
# make sure multicast is registered with the local IP used to reach this host
server._server.register_multicast_from_host(host)
instance_list = []
_LOGGER.debug("Beginning ECHONET node discovery")
await server.discover(host)
# Timeout after 10 seconds
for x in range(0, 1000):
await asyncio.sleep(0.01)
if "discovered" in list(server._state[host]):
_LOGGER.debug("ECHONET Node Discovery Successful!")
break
if "discovered" not in list(server._state[host]):
_LOGGER.debug("ECHONET Node Discovery Failed!")
raise ErrorConnect("cannot_connect")
state = server._state[host]
uid = state["uid"]
# check ip addr changed
if newhost:
config_entry = None
old_host = None
entries = hass.config_entries.async_entries(DOMAIN)
entry = None
instances = None
for entry in entries:
instances = []
_data = entry.data
for _instance in _data.get("instances", []):
instance = _instance.copy()
if old_host or instance.get("uid") == uid:
old_host = instance["host"]
instance["host"] = host
instances.append(instance)
if old_host:
config_entry = entry
_LOGGER.debug(
f"ECHONET registed node found uid is {uid}, conig entry id is {entry.entry_id}."
)
break
if old_host and entry and instances and config_entry:
_LOGGER.debug(
f"ECHONET registed node IP changed from {old_host} to {host}."
)
_LOGGER.debug(f"New instances data is {instances}")
if server._state.get(old_host):
server._state[host] = server._state.pop(old_host)
hass.config_entries.async_update_entry(
config_entry, data={"host": host, "instances": instances}
)
# Wait max 30 secs for entry loaded
for x in range(0, 300):
await asyncio.sleep(0.1)
if entry.state == ConfigEntryState.LOADED:
await hass.config_entries.async_reload(entry.entry_id)
break
raise ErrorIpChanged(host)
manufacturer = state["manufacturer"]
host_product_code = state.get("product_code")
if type(host_product_code) == str:
host_product_code = str.strip(host_product_code)
if not isinstance(manufacturer, str):
# If unable to resolve the manufacturer,
# the raw identification number will be passed as int.
_LOGGER.warn(
f"{host} - Unable to resolve the manufacturer name - {manufacturer}. "
+ "Please report the manufacturer name of your device at the issue tracker on GitHub!"
)
manufacturer = f"Unknown({manufacturer})"
for eojgc in list(state["instances"].keys()):
for eojcc in list(state["instances"][eojgc].keys()):
for instance in list(state["instances"][eojgc][eojcc].keys()):
_LOGGER.debug(f"instance is {instance}")
cnt = 0
while (
await server.getAllPropertyMaps(host, eojgc, eojcc, instance)
is False
):
cnt += 1
if cnt > 2:
raise ErrorConnect("cannot_get_property_maps")
_LOGGER.debug(
f"{host} - ECHONET Instance {eojgc}-{eojcc}-{instance} map attributes discovered!"
)
ntfmap = state["instances"][eojgc][eojcc][instance].get(ENL_STATMAP, [])
getmap = state["instances"][eojgc][eojcc][instance][ENL_GETMAP]
setmap = state["instances"][eojgc][eojcc][instance][ENL_SETMAP]
uidi = f"{uid}-{eojgc}-{eojcc}-{instance}"
name = None
if host_product_code == "WTY2001" and eojcc == 0x91:
# Panasonic WTY2001 Advanced Series Link Plus Wireless Adapter
await server.echonetMessage(
host,
eojgc,
eojcc,
instance,
GET,
[{"EPC": 0xFD}, {"EPC": 0xFE}],
)
# Use Use HW ID because the instance number is uncertain
# https://github.com/scottyphillips/echonetlite_homeassistant/issues/117#issuecomment-1929151918
uidi = _null_padded_optional_string(
state["instances"][eojgc][eojcc][instance][0xFE]
)
name = _null_padded_optional_string(
state["instances"][eojgc][eojcc][instance][0xFD]
)
instance_list.append(
{
"host": host,
"name": name,
"eojgc": eojgc,
"eojcc": eojcc,
"eojci": instance,
"ntfmap": ntfmap,
"getmap": getmap,
"setmap": setmap,
"uid": uid, # Deprecated, for backwards compatibility
"uidi": uidi,
"manufacturer": manufacturer,
"host_product_code": host_product_code,
}
)
return instance_list
async def async_discover_newhost(hass, host):
_LOGGER.debug(f"received newip discovery: {host}")
if host not in _detected_hosts.keys():
try:
instance_list = await enumerate_instances(hass, host, newhost=True)
_LOGGER.debug(f"ECHONET Node detected in {host}")
except ErrorConnect as e:
_LOGGER.debug(f"ECHONET Node Error Connect ({e})")
except ErrorIpChanged as e:
_LOGGER.debug(f"ECHONET Detected Node IP Changed to '{e}'")
else:
if len(instance_list):
_detected_hosts.update({host: instance_list})
else:
_LOGGER.debug(f"ECHONET Node not found in {host}")
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for echonetlite."""
host = None
title = ""
discover_task = None
instance_list = None
instances = None
VERSION = 1
async def init_discover(self):
async def discover_callback(host):
await async_discover_newhost(self.hass, host)
if DOMAIN in self.hass.data: # maybe set up by config entry?
_LOGGER.debug("API listener has already been setup previously..")
server = self.hass.data[DOMAIN]["api"]
_init_server = None
else:
udp = UDPServer()
udp.run("0.0.0.0", 3610, loop=self.hass.loop)
server = ECHONETAPIClient(server=udp)
server._debug_flag = True
server._logger = _LOGGER.debug
server._message_timeout = 300
server._discover_callback = discover_callback
_init_server = server
await server.discover()
# Timeout after 30 seconds
for x in range(0, 3000):
await asyncio.sleep(0.01)
if len(_detected_hosts):
_LOGGER.debug("ECHONET Any Node Discovery Successful!")
break
if _init_server:
_init_server._server._sock.close()
del _init_server
_init_server = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
errors = {}
"""Handle the initial step."""
scm = STEP_USER_DATA_SCHEMA
if user_input is None or user_input.get("host") == WORD_OF_AUTO_DISCOVERY:
step = "user_man"
if (
user_input
and user_input.get("host") == WORD_OF_AUTO_DISCOVERY
and not len(_detected_hosts)
):
await self.init_discover()
if len(_detected_hosts):
host = list(_detected_hosts.keys()).pop(0)
title = _detected_hosts[host][0]["manufacturer"]
if _detected_hosts[host][0]["host_product_code"]:
title += " " + _detected_hosts[host][0]["host_product_code"]
else:
if user_input is None:
host = title = WORD_OF_AUTO_DISCOVERY
step = "user"
else:
host = ""
title = ""
errors["base"] = "not_found"
scm = scm.extend(
{
vol.Required("host", default=host): str,
vol.Required("title", default=title): str,
}
)
return self.async_show_form(step_id=step, data_schema=scm, errors=errors)
try:
self.instance_list = await enumerate_instances(
self.hass, user_input["host"]
)
_LOGGER.debug("Node detected")
except ErrorConnect as e:
errors["base"] = f"{e}"
else:
self.host = user_input["host"]
self.title = user_input["title"]
return await self.async_step_finish(user_input)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_user_man(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
return await self.async_step_user(user_input)
async def async_step_finish(self, user_input=None):
if len(_detected_hosts) and self.host in _detected_hosts.keys():
_detected_hosts.pop(self.host)
return self.async_create_entry(
title=self.title,
data={"host": self.host, "instances": self.instance_list},
options={"other_mode": "as_off"},
)
@staticmethod
@callback
def async_get_options_flow(config_entry):
return OptionsFlowHandler(config_entry)
class ErrorConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
class ErrorIpChanged(HomeAssistantError):
"""Error to indicate we cannot connect."""
class OptionsFlowHandler(config_entries.OptionsFlow):
def __init__(self, config):
self._config_entry = config
self._data = {}
async def async_step_init(self, user_input=None):
"""Manage the options."""
data_schema_structure = {}
# Handle HVAC and Air Cleaner User configurable options
for instance in self._config_entry.data["instances"]:
if (
instance["eojgc"] == 0x01 and instance["eojcc"] == 0x30
): # HomeAirConditioner
ha_swing_list = []
for option in list(USER_OPTIONS.keys()):
if option in instance["setmap"]:
if option in [ENL_AIR_VERT, ENL_AUTO_DIRECTION, ENL_SWING_MODE]:
ha_swing_list.append(option)
if (
self._config_entry.options.get(
USER_OPTIONS[option]["option"]
)
is not None
):
option_default = self._config_entry.options.get(
USER_OPTIONS[option]["option"]
)
else:
if isinstance(USER_OPTIONS[option]["option_list"], list):
# single select
option_default = USER_OPTIONS[option]["option_list"][0][
"value"
]
elif isinstance(USER_OPTIONS[option]["option_list"], dict):
# multi selectable
option_default = list(
USER_OPTIONS[option]["option_list"].keys()
)
else:
option_default = []
data_schema_structure.update(
{
vol.Optional(
USER_OPTIONS[option]["option"],
default=option_default,
): cv.multi_select(USER_OPTIONS[option]["option_list"])
}
)
# Handle setting Climate entity UI swing mode
if len(ha_swing_list) > 0:
option_list = {}
for opt in ha_swing_list:
option_list.update(USER_OPTIONS[opt]["option_list"])
if ENL_AIR_VERT in instance["setmap"]:
for del_key in [
"auto",
"non-auto",
"auto-horiz",
"not-used",
"horiz",
"vert-horiz",
]:
option_list.pop(del_key, None)
if self._config_entry.options.get(OPTION_HA_UI_SWING) is not None:
option_default = self._config_entry.options.get(
OPTION_HA_UI_SWING
)
else:
option_default = list(option_list.keys())
data_schema_structure.update(
{
vol.Optional(
OPTION_HA_UI_SWING,
default=option_default,
): cv.multi_select(option_list)
}
)
# Handle setting temperature ranges for various modes of operation
for option in list(TEMP_OPTIONS.keys()):
default_temp = TEMP_OPTIONS[option]["min"]
if self._config_entry.options.get(option) is not None:
default_temp = self._config_entry.options.get(option)
else:
default_temp = TEMP_OPTIONS[option]["default"]
data_schema_structure.update(
{
vol.Required(option, default=default_temp): vol.All(
vol.Coerce(int),
vol.Range(
min=TEMP_OPTIONS[option]["min"],
max=TEMP_OPTIONS[option]["max"],
),
)
}
)
# Handle setting for the operation mode "Other"
option_default = "as_off"
if self._config_entry.options.get(CONF_OTHER_MODE) is not None:
option_default = self._config_entry.options.get(CONF_OTHER_MODE)
data_schema_structure.update(
{
vol.Optional(
USER_OPTIONS[ENL_HVAC_MODE]["option"],
default=option_default,
): selector(
{
"select": {
"options": USER_OPTIONS[ENL_HVAC_MODE][
"option_list"
],
"mode": "dropdown",
}
}
)
}
)
elif instance["eojgc"] == 0x01 and instance["eojcc"] == 0x35: # AirCleaner
for option in list(USER_OPTIONS.keys()):
if option in instance["setmap"]:
option_default = []
if (
self._config_entry.options.get(
USER_OPTIONS[option]["option"]
)
is not None
):
option_default = self._config_entry.options.get(
USER_OPTIONS[option]["option"]
)
data_schema_structure.update(
{
vol.Optional(
USER_OPTIONS[option]["option"],
default=option_default,
): cv.multi_select(USER_OPTIONS[option]["option_list"])
}
)
for key, option in MISC_OPTIONS.items():
if "min" in option and "max" in option:
_type = vol.All(
vol.Coerce(option["type"]),
vol.Range(min=option["min"], max=option["max"]),
)
else:
_type = option["type"]
if type(option["default"]) == list and type(option["default"][0]) == dict:
option_default = None
for instance in self._config_entry.data["instances"]:
option_default = (
option["default"][0]
.get(instance["eojgc"], {})
.get(instance["eojcc"])
)
if option_default != None:
break
if option_default == None:
option_default = option["default"][1]
else:
option_default = option["default"]
data_schema_structure.update(
{
vol.Required(
key,
default=self._config_entry.options.get(key, option_default),
): _type
}
)
if user_input is not None or not any(data_schema_structure):
if user_input is not None:
self._data.update(user_input)
return self.async_create_entry(title="", data=self._data)
# return await self.async_step_misc()
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(data_schema_structure),
)
async def async_step_misc(self, user_input=None):
"""Manage the options."""
data_schema_structure = {}
# for key, option in MISC_OPTIONS.items():
# data_schema_structure.update({
# vol.Required(
# CONF_FORCE_POLLING,
# default=self._config_entry.options.get(key, option['default'])
# ): option['type']
# })
if user_input is not None:
self._data.update(user_input)
return self.async_create_entry(title="", data=self._data)
return self.async_show_form(
step_id="misc",
data_schema=vol.Schema(data_schema_structure),
)
================================================
FILE: custom_components/echonetlite/const.py
================================================
"""Constants for the echonetlite integration."""
from homeassistant.const import (
CONF_ICON,
CONF_SERVICE,
CONF_TYPE,
CONF_SERVICE_DATA,
CONF_UNIT_OF_MEASUREMENT,
CONF_NAME,
CONF_MINIMUM,
CONF_MAXIMUM,
PERCENTAGE,
UnitOfEnergy,
UnitOfTime,
UnitOfVolume,
UnitOfVolumeFlowRate,
)
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.sensor.const import (
CONF_STATE_CLASS,
SensorStateClass,
SensorDeviceClass,
)
from homeassistant.components.number.const import (
NumberDeviceClass,
)
from pychonet.ElectricBlind import (
ENL_OPENING_LEVEL,
ENL_BLIND_ANGLE,
ENL_OPENCLOSE_STATUS,
)
from pychonet.GeneralLighting import ENL_BRIGHTNESS, ENL_COLOR_TEMP
from pychonet.HomeAirConditioner import (
ENL_HVAC_MODE,
ENL_FANSPEED,
ENL_AIR_VERT,
ENL_AIR_HORZ,
ENL_AUTO_DIRECTION,
ENL_HVAC_SET_TEMP,
ENL_HVAC_SILENT_MODE,
ENL_SWING_MODE,
FAN_SPEED,
AIRFLOW_VERT,
AIRFLOW_HORIZ,
AUTO_DIRECTION,
SWING_MODE,
)
from pychonet.EchonetInstance import ENL_STATUS, ENL_ON, ENL_OFF
from pychonet.lib.const import (
ENL_CUMULATIVE_POWER,
ENL_FAULT_DESCRIPTION,
ENL_FAULT_STATUS,
ENL_INSTANTANEOUS_POWER,
)
from pychonet.lib.epc_functions import DATA_STATE_CLOSE, DATA_STATE_OPEN
from pychonet.CeilingFan import (
ENL_BUZZER,
ENL_FANSPEED_PERCENT,
ENL_FAN_DIRECTION,
ENL_FAN_LIGHT_BRIGHTNESS,
ENL_FAN_LIGHT_COLOR_TEMP,
ENL_FAN_LIGHT_MODE,
ENL_FAN_LIGHT_NIGHT_BRIGHTNESS,
ENL_FAN_LIGHT_STATUS,
ENL_FAN_OSCILLATION,
)
DOMAIN = "echonetlite"
CONF_ENSURE_ON = "ensureon"
CONF_OTHER_MODE = "other_mode"
CONF_FORCE_POLLING = "force_polling"
CONF_ENABLE_SUPER_ENERGY = "super_energy"
CONF_BATCH_SIZE_MAX = "batch_size_max"
CONF_ON_VALUE = "on_val"
CONF_OFF_VALUE = "off_val"
CONF_DISABLED_DEFAULT = "disabled_default"
CONF_MULTIPLIER = "multiplier"
CONF_MULTIPLIER_OPCODE = "multiplier_opcode"
CONF_MULTIPLIER_OPTIONAL_OPCODE = "multiplier_optional_opcode"
CONF_ICON_POSITIVE = "icon_positive"
CONF_ICON_NEGATIVE = "icon_negative"
CONF_ICON_ZERO = "icon_zero"
CONF_ICONS = "icons"
CONF_AS_ZERO = "as_zero"
CONF_MAX_OPC = "max_opc"
CONF_BYTE_LENGTH = "byte_len"
DATA_STATE_ON = "on"
DATA_STATE_OFF = "off"
TYPE_SWITCH = "switch"
TYPE_SELECT = "select"
TYPE_TIME = "time"
TYPE_NUMBER = "number"
TYPE_DATA_DICT = "type_data_dict"
TYPE_DATA_ARRAY_WITH_SIZE_OPCODE = "type_data_array_with_size_opcode"
SERVICE_SET_ON_TIMER_TIME = "set_on_timer_time"
SERVICE_SET_INT_1B = "set_value_int_1b"
OPEN = "open"
CLOSE = "close"
STOP = "stop"
DEVICE_CLASS_ECHONETLITE_LIGHT_SCENE = "echonetlite_light_scene"
SWITCH_POWER = {DATA_STATE_ON: ENL_ON, DATA_STATE_OFF: ENL_OFF}
SWITCH_BINARY = {DATA_STATE_ON: 0x41, DATA_STATE_OFF: 0x42}
SWITCH_BINARY_INVERT = {DATA_STATE_ON: 0x42, DATA_STATE_OFF: 0x41}
HVAC_SELECT_OP_CODES = {
0xA0: FAN_SPEED,
0xA1: AUTO_DIRECTION,
0xA3: SWING_MODE,
0xA5: AIRFLOW_HORIZ,
0xA4: AIRFLOW_VERT,
}
FAN_SELECT_OP_CODES = {0xA0: FAN_SPEED}
COVER_SELECT_OP_CODES = {0xE0: {OPEN: 0x41, CLOSE: 0x42, STOP: 0x43}}
ENL_TIMER_SETTING = 0x97
ENL_SUPER_CODES = {
ENL_STATUS: {CONF_TYPE: BinarySensorDeviceClass.POWER},
ENL_INSTANTANEOUS_POWER: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
ENL_CUMULATIVE_POWER: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
CONF_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
CONF_MULTIPLIER: 0.001,
},
ENL_TIMER_SETTING: {
CONF_ICON: "mdi:clock-outline",
TYPE_TIME: True,
},
ENL_FAULT_STATUS: {CONF_TYPE: BinarySensorDeviceClass.PROBLEM},
ENL_FAULT_DESCRIPTION: {
CONF_TYPE: SensorDeviceClass.ENUM,
TYPE_DATA_DICT: ["fault classification", "error code"],
},
}
ENL_SUPER_ENERGES = {ENL_INSTANTANEOUS_POWER, ENL_CUMULATIVE_POWER}
ENL_OP_CODES = {
0x00: { # Sensor-related Device
0x08: { # Visitor sensor class
0xB0: {
CONF_ICON: "mdi:motion-sensor",
}, # Detection threshold level
0xB1: {
CONF_ICON: "mdi:motion-sensor",
}, # Visitor detection status
0xBE: {
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.DURATION,
CONF_UNIT_OF_MEASUREMENT: UnitOfTime.SECONDS,
CONF_MINIMUM: 0,
CONF_MAXIMUM: 0xFFFD,
CONF_MULTIPLIER: 10,
},
}, # Visitor detection holding time
},
0x11: { # Temperature sensor
0xE0: {
CONF_ICON: "mdi:thermometer",
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
},
},
0x01: { # Air Conditioner-related Device
0x30: { # Home air conditioner
# 0xB3: { # for develop test
# CONF_TYPE: SensorDeviceClass.TEMPERATURE,
# CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
# TYPE_NUMBER: { # Make Number input entity if settable value
# CONF_TYPE: NumberDeviceClass.TEMPERATURE, # NumberDeviceClass.x
# CONF_AS_ZERO: 0x1, # Value as zero
# CONF_MINIMUM: 0x0, # Minimum value
# CONF_MAXIMUM: 0x32, # Maximum value
# CONF_MAX_OPC: None, # OPC of max value
# CONF_BYTE_LENGTH: 0x1, # Data byte length
# TYPE_SWITCH: { # Additional switch
# CONF_NAME: "Auto", # Additionale name
# CONF_ICON: "mdi:thermometer",
# CONF_SERVICE_DATA: {DATA_STATE_ON: 23, DATA_STATE_OFF: 22},
# },
# },
# },
0xB4: { # Humidity setting in dry mode
CONF_TYPE: SensorDeviceClass.HUMIDITY,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.HUMIDITY,
CONF_MINIMUM: 30,
CONF_MAXIMUM: 90,
},
CONF_SERVICE: [
SERVICE_SET_INT_1B
], # For backward compatibility (Deprecated)
},
0xBA: {
CONF_TYPE: SensorDeviceClass.HUMIDITY,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
0xBE: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
0xBB: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
0xA0: {
CONF_ICON: "mdi:fan",
},
0xA1: {
CONF_ICON: "mdi:shuffle-variant",
},
0xA3: {
CONF_ICON: "mdi:arrow-oscillating",
},
0xA5: {
CONF_ICON: "mdi:tailwind",
},
0xA4: {
CONF_ICON: "mdi:tailwind",
},
},
0x35: { # Air cleaner
0xE1: {
CONF_ICON: "mdi:air-filter",
},
0xA0: {
CONF_ICON: "mdi:fan",
},
0xC1: {
CONF_ICON: "mdi:smoking",
},
0xC2: {
CONF_ICON: "mdi:weather-sunny",
},
0xC0: {
CONF_ICON: "mdi:flower-pollen",
},
},
},
0x02: { # Housing/Facilities-related Device
0x60: { # Electrically operated blind/shade
0xE0: { # Configured as Cover but left for backward compatibility
CONF_ICON: "mdi:roller-shade",
CONF_ICONS: {
OPEN: "mdi:roller-shade",
CLOSE: "mdi:roller-shade-closed",
STOP: "mdi:roller-shade",
},
CONF_DISABLED_DEFAULT: True,
},
0xD2: {
CONF_UNIT_OF_MEASUREMENT: UnitOfTime.SECONDS,
TYPE_NUMBER: {
CONF_MAXIMUM: 0x00,
CONF_MAXIMUM: 0xFD,
},
}, # Operation time
},
0x61: { # Electrically operated shutter
0xE0: { # Configured as Cover but left for backward compatibility
CONF_ICON: "mdi:window-shutter-open",
CONF_ICONS: {
OPEN: "mdi:window-shutter-open",
CLOSE: "mdi:window-shutter",
STOP: "mdi:window-shutter-open",
},
CONF_DISABLED_DEFAULT: True,
},
0xD2: {
CONF_UNIT_OF_MEASUREMENT: UnitOfTime.SECONDS,
TYPE_NUMBER: {
CONF_MAXIMUM: 0x00,
CONF_MAXIMUM: 0xFD,
},
}, # Operation time
},
0x62: { # Electrically operated curtain
0xE0: { # Configured as Cover but left for backward compatibility
CONF_ICON: "mdi:curtains",
CONF_ICONS: {
OPEN: "mdi:curtains",
CLOSE: "mdi:curtains-closed",
STOP: "mdi:curtains",
},
CONF_DISABLED_DEFAULT: True,
},
0xD2: {
CONF_UNIT_OF_MEASUREMENT: UnitOfTime.SECONDS,
TYPE_NUMBER: {
CONF_MAXIMUM: 0x00,
CONF_MAXIMUM: 0xFD,
},
}, # Operation time
},
0x63: { # Electrically operated rain sliding door/shutter
0xE0: { # Configured as Cover but left for backward compatibility
CONF_ICON: "mdi:door-sliding-open",
CONF_ICONS: {
OPEN: "mdi:door-sliding-open",
CLOSE: "mdi:door-sliding",
STOP: "mdi:door-sliding-open",
},
CONF_DISABLED_DEFAULT: True,
},
0xD2: {
CONF_UNIT_OF_MEASUREMENT: UnitOfTime.SECONDS,
TYPE_NUMBER: {
CONF_MAXIMUM: 0x00,
CONF_MAXIMUM: 0xFD,
},
}, # Operation time
},
0x64: { # Electrically operated gate
0xE0: { # Configured as Cover but left for backward compatibility
CONF_ICON: "mdi:boom-gate-up-outline",
CONF_ICONS: {
OPEN: "mdi:boom-gate-up-outline",
CLOSE: "mdi:boom-gate-outline",
STOP: "mdi:boom-gate-up-outline",
},
CONF_DISABLED_DEFAULT: True,
},
0xD2: {
CONF_UNIT_OF_MEASUREMENT: UnitOfTime.SECONDS,
TYPE_NUMBER: {
CONF_MAXIMUM: 0x00,
CONF_MAXIMUM: 0xFD,
},
}, # Operation time
},
0x65: { # Electrically operated window
0xE0: { # Configured as Cover but left for backward compatibility
CONF_ICON: "mdi:window-open-variant",
CONF_ICONS: {
OPEN: "mdi:window-open-variant",
CLOSE: "mdi:window-closed-variant",
STOP: "mdi:window-open-variant",
},
CONF_DISABLED_DEFAULT: True,
},
0xD2: {
CONF_UNIT_OF_MEASUREMENT: UnitOfTime.SECONDS,
TYPE_NUMBER: {
CONF_MAXIMUM: 0x00,
CONF_MAXIMUM: 0xFD,
},
}, # Operation time
},
0x66: { # Automatically operated entrance door/sliding door
0xE0: { # Configured as Cover but left for backward compatibility
CONF_ICON: "mdi:door-sliding-open",
CONF_ICONS: {
OPEN: "mdi:door-sliding-open",
CLOSE: "mdi:door-sliding",
STOP: "mdi:door-sliding-open",
},
CONF_DISABLED_DEFAULT: True,
},
0xD2: {
CONF_UNIT_OF_MEASUREMENT: UnitOfTime.SECONDS,
TYPE_NUMBER: {
CONF_MAXIMUM: 0x00,
CONF_MAXIMUM: 0xFD,
},
}, # Operation time
},
0x6B: { # Electric water heater
# 0xB0: , # "Automatic water heating setting",
# 0xB1: , # "Automatic water temperature control setting",
# 0xB2: , # "Water heater status",
0xB3: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.TEMPERATURE,
CONF_MINIMUM: 30,
CONF_MAXIMUM: 90,
},
}, # "Water heating temperature setting",
0xB4: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
}, # "Manual water heating stop days setting",
# 0xB5: , # "Relative time setting value for manual water heating OFF",
# 0xB6: , # Tank operation mode setting",
# 0xC0: , # Daytime reheating permission setting",
0xC1: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
}, # Measured temperature of water in water heater",
# 0xC2: , # Alarm status",
# 0xC3: , # Hot water supply status",
# 0xC4: , # Relative time setting for keeping bath temperature",
0xD1: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.TEMPERATURE,
CONF_MINIMUM: 30,
CONF_MAXIMUM: 90,
},
}, # Temperature of supplied water setting",
0xD3: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.TEMPERATURE,
CONF_MINIMUM: 30,
CONF_MAXIMUM: 90,
},
}, # Bath water temperature setting",
0xE0: {
CONF_UNIT_OF_MEASUREMENT: PERCENTAGE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_MAXIMUM: 100,
},
}, # Bath water volume setting",
0xE1: {
CONF_TYPE: SensorDeviceClass.WATER,
CONF_UNIT_OF_MEASUREMENT: UnitOfVolume.LITERS,
}, # Measured amount of water remaining in tank",
0xE2: {
CONF_TYPE: SensorDeviceClass.WATER,
CONF_UNIT_OF_MEASUREMENT: UnitOfVolume.LITERS,
}, # Tank capacity",
# 0xE3: , # Automatic bath water heating mode setting",
# 0xE9: , # Bathroom priority setting",
# 0xEA: , # Bath operation status monitor",
# 0xE4: , # Manual bath reheating operation setting",
# 0xE5: , # Manual bath hot water addition function setting",
# 0xE6: , # Manual slight bath water temperature lowering function setting",
0xE7: {
CONF_UNIT_OF_MEASUREMENT: UnitOfVolume.LITERS,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_MAXIMUM: 0xFD,
},
}, # Bath water volume setting 1",
0xE8: {
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_AS_ZERO: 0x30,
CONF_MINIMUM: 0x31,
CONF_MAXIMUM: 0x38,
},
}, # Bath water volume setting 2",
0xEE: {
CONF_UNIT_OF_MEASUREMENT: UnitOfVolume.LITERS,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_MAXIMUM: 0xFFFD,
CONF_BYTE_LENGTH: 2,
},
}, # Bath water volume setting 3",
0xD4: {
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_MINIMUM: 0x01,
CONF_MAXIMUM: 0xFF,
CONF_MAX_OPC: 0xD5,
},
}, # Bath water volume setting 4",
0x90: {
CONF_ICON: "mdi:timer",
}, # ON timer reservation setting",
0x91: {
CONF_ICON: "mdi:timer-outline",
}, # ON timer setting",
0xD6: {
CONF_UNIT_OF_MEASUREMENT: PERCENTAGE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_MAXIMUM: 0x64,
},
}, # Volume setting",
# 0xD7: , # Mute setting",
0xD8: {
CONF_TYPE: SensorDeviceClass.WATER,
CONF_UNIT_OF_MEASUREMENT: UnitOfVolume.LITERS,
}, # Remaining hot water volume",
0xDB: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
}, # Rated power consumption of H/P unit in wintertime",
0xDC: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
}, # Rated power consumption of H/P unit in in-between seasons",
0xDD: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
}, # Rated power consumption of H/P unit in summertime",
0xCB: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_DATA_DICT: ["10:00", "13:00", "15:00", "17:00"],
},
0xCC: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_DATA_DICT: ["10:00", "13:00", "15:00", "17:00"],
},
0xCE: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_DATA_DICT: ["13:00", "15:00", "17:00"],
},
0xCF: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_DATA_DICT: ["13:00", "15:00", "17:00"],
},
},
0x6F: { # Electric lock
0xE0: {
CONF_ICON: "mdi:lock",
CONF_ENSURE_ON: ENL_STATUS,
},
0xE1: {
CONF_ICON: "mdi:lock",
CONF_ENSURE_ON: ENL_STATUS,
},
0xE6: {
CONF_ICON: None,
CONF_ENSURE_ON: ENL_STATUS,
},
},
0x72: { # Hot water generator
0x90: {
CONF_ICON: "mdi:timer",
},
0x91: { # Sensor with service
CONF_ICON: "mdi:timer-outline",
CONF_SERVICE: [
SERVICE_SET_ON_TIMER_TIME
], # For backward compatibility (Deprecated)
},
0xD1: { # Sensor
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.TEMPERATURE,
CONF_MINIMUM: 30,
CONF_MAXIMUM: 90,
},
CONF_SERVICE: [
SERVICE_SET_INT_1B
], # For backward compatibility (Deprecated)
},
0xE1: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.TEMPERATURE,
CONF_MINIMUM: 30,
CONF_MAXIMUM: 90,
},
CONF_SERVICE: [
SERVICE_SET_INT_1B
], # For backward compatibility (Deprecated)
},
0xE3: {
CONF_ICON: "mdi:bathtub-outline",
},
0xE4: {
CONF_ICON: "mdi:heat-wave",
},
0xE7: {
CONF_TYPE: SensorDeviceClass.WATER,
CONF_UNIT_OF_MEASUREMENT: UnitOfVolume.LITERS,
},
0xEE: {
CONF_TYPE: SensorDeviceClass.WATER,
CONF_UNIT_OF_MEASUREMENT: UnitOfVolume.LITERS,
},
},
0x73: { # Bathroom dryer
0xBA: {
CONF_TYPE: SensorDeviceClass.HUMIDITY,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
0xBB: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
0xE0: {
CONF_ICON: "mdi:motion-sensor",
},
},
0x79: { # Home solar power generation
0xA0: {
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
CONF_UNIT_OF_MEASUREMENT: PERCENTAGE,
TYPE_NUMBER: {
CONF_MAXIMUM: 0x64,
},
},
0xA1: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.POWER,
CONF_MAXIMUM: 0xFFFD,
CONF_BYTE_LENGTH: 0x02,
},
},
0xE0: {
CONF_ICON: "mdi:solar-power-variant-outline",
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
CONF_ICON_POSITIVE: "mdi:solar-power-variant",
CONF_ICON_NEGATIVE: "mdi:solar-power-variant-outline",
CONF_ICON_ZERO: "mdi:solar-power-variant-outline",
},
0xE1: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
},
0xE3: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
},
0xE5: {
CONF_ICON: "mdi:percent",
CONF_UNIT_OF_MEASUREMENT: PERCENTAGE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_MAXIMUM: 0x64,
},
},
0xE6: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.POWER,
CONF_MAXIMUM: 0xFFFD,
CONF_BYTE_LENGTH: 0x02,
},
},
0xE7: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.POWER,
CONF_MAXIMUM: 0xFFFD,
CONF_BYTE_LENGTH: 0x02,
},
},
0xE8: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.POWER,
CONF_MAXIMUM: 0xFFFD,
CONF_BYTE_LENGTH: 0x02,
},
},
0xE9: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.POWER,
CONF_MAXIMUM: 0xFFFD,
CONF_BYTE_LENGTH: 0x02,
},
},
},
0x7B: { # Floor heater
0xE0: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.TEMPERATURE,
CONF_MINIMUM: 16,
CONF_MAXIMUM: 40,
TYPE_SWITCH: {
CONF_NAME: "Auto",
CONF_SERVICE_DATA: {DATA_STATE_ON: 0x41, DATA_STATE_OFF: 16},
},
},
},
0xE1: {
CONF_ICON: "mdi:thermometer",
CONF_TYPE: None,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_AS_ZERO: 0x30,
CONF_MINIMUM: 0x31,
CONF_MAXIMUM: 0x3F,
CONF_MAX_OPC: 0xD1,
TYPE_SWITCH: {
CONF_NAME: "Auto",
CONF_SERVICE_DATA: {DATA_STATE_ON: 0x41, DATA_STATE_OFF: 0x31},
},
},
},
0xE2: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
0xE3: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
0x90: {
CONF_ICON: "mdi:timer",
},
0x91: {
CONF_ICON: "mdi:timer-outline",
},
0x94: {
CONF_ICON: "mdi:timer",
},
0x95: {
CONF_ICON: "mdi:timer-outline",
},
},
0x7C: { # Fuel cell
0xC2: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
0xC4: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
0xC5: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
},
0xCC: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
0xCD: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
},
0xC7: {
CONF_TYPE: SensorDeviceClass.GAS,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
CONF_UNIT_OF_MEASUREMENT: UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
},
0xC8: {
CONF_TYPE: SensorDeviceClass.GAS,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
CONF_UNIT_OF_MEASUREMENT: UnitOfVolume.LITERS,
},
},
0x7D: { # Storage battery
0xA0: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
},
0xA1: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
},
0xA2: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
},
0xA3: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
},
0xA4: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
},
0xA5: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
},
0xA6: {
CONF_TYPE: SensorDeviceClass.BATTERY,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.BATTERY,
CONF_MAXIMUM: 0x64,
},
},
0xA7: {
CONF_TYPE: SensorDeviceClass.BATTERY,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.BATTERY,
CONF_MAXIMUM: 0x64,
},
},
0xA8: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
},
0xA9: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
},
0xAA: {
CONF_ICON: "mdi:battery",
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.ENERGY,
CONF_MAXIMUM: 0x3B9AC9FF,
CONF_BYTE_LENGTH: 0x04,
},
},
0xAB: {
CONF_ICON: "mdi:battery",
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.ENERGY,
CONF_MAXIMUM: 0x3B9AC9FF,
CONF_BYTE_LENGTH: 0x04,
},
},
0xD0: {
CONF_ICON: "mdi:battery",
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
},
0xD1: {
CONF_MULTIPLIER: 0.1,
CONF_UNIT_OF_MEASUREMENT: "Ah",
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
0xD2: {
CONF_TYPE: SensorDeviceClass.VOLTAGE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
0xD3: {
CONF_ICON_POSITIVE: "mdi:battery-arrow-up",
CONF_ICON_NEGATIVE: "mdi:battery-arrow-down",
CONF_ICON_ZERO: "mdi:battery",
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
0xD6: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
},
0xD8: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
},
0xE0: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
},
0xE2: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
},
0xE4: {
CONF_TYPE: SensorDeviceClass.BATTERY,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
0xE5: {
CONF_TYPE: SensorDeviceClass.BATTERY,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
0xE7: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.ENERGY,
CONF_MAXIMUM: 0x3B9AC9FF,
CONF_BYTE_LENGTH: 0x04,
},
},
0xE8: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.ENERGY,
CONF_MAXIMUM: 0x3B9AC9FF,
CONF_BYTE_LENGTH: 0x04,
},
},
0xEB: {
CONF_ICON: "mdi:battery",
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.POWER,
CONF_MAXIMUM: 0x3B9AC9FF,
CONF_BYTE_LENGTH: 0x04,
},
},
0xEC: {
CONF_ICON: "mdi:battery",
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.POWER,
CONF_MAXIMUM: 0x3B9AC9FF,
CONF_BYTE_LENGTH: 0x04,
},
},
0xEF: {
CONF_TYPE: SensorDeviceClass.VOLTAGE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
},
0x80: { # Electric energy meter
0xE0: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
CONF_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
CONF_MULTIPLIER_OPCODE: 0xE2,
},
0xE2: {
CONF_DISABLED_DEFAULT: True,
},
},
0x81: { # Water flow meter
0xE0: {
# CONF_ICON: "mdi:water",
CONF_TYPE: SensorDeviceClass.WATER,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
CONF_MULTIPLIER_OPCODE: 0xE1,
},
0xE1: {
CONF_DISABLED_DEFAULT: True,
},
},
0x82: { # Gas meter
0xE0: {
# CONF_ICON: "mdi:gas-burner",
CONF_TYPE: SensorDeviceClass.GAS,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
CONF_MULTIPLIER: 0.001,
}
},
0x87: { # Distribution panel metering
0xC2: {
CONF_DISABLED_DEFAULT: True,
},
0xB3: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
CONF_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
TYPE_DATA_ARRAY_WITH_SIZE_OPCODE: 0xB1,
CONF_MULTIPLIER_OPCODE: 0xC2,
},
0xB7: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_DATA_ARRAY_WITH_SIZE_OPCODE: 0xB1,
},
0xC0: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
CONF_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
CONF_MULTIPLIER_OPCODE: 0xC2,
},
0xC1: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
CONF_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
CONF_MULTIPLIER_OPCODE: 0xC2,
},
0xC6: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
0xC7: {
CONF_TYPE: SensorDeviceClass.CURRENT,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_DATA_DICT: ["r_phase_amperes", "t_phase_amperes"],
CONF_DISABLED_DEFAULT: True,
},
0xC8: {
CONF_TYPE: SensorDeviceClass.VOLTAGE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_DATA_DICT: ["r_sn_voltage", "sn_t_voltage"],
CONF_DISABLED_DEFAULT: True,
},
},
0x88: { # Low voltage smart electric energy meter
0xD3: {
CONF_DISABLED_DEFAULT: True,
},
0xE0: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
CONF_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
CONF_MULTIPLIER_OPCODE: 0xE1,
CONF_MULTIPLIER_OPTIONAL_OPCODE: 0xD3,
},
0xE1: {
CONF_DISABLED_DEFAULT: True,
},
0xE3: {
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
CONF_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
CONF_MULTIPLIER_OPCODE: 0xE1,
CONF_MULTIPLIER_OPTIONAL_OPCODE: 0xD3,
},
0xE7: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
0xE8: {
CONF_TYPE: SensorDeviceClass.CURRENT,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_DATA_DICT: ["r_phase_amperes", "t_phase_amperes"],
},
# 0xEA: {
# TYPE_DATA_DICT: ["time", "culmative_value"],
# },
# 0xEB: {
# TYPE_DATA_DICT: ["time", "culmative_value"],
# },
0xD3: {CONF_DISABLED_DEFAULT: True},
0xE1: {CONF_DISABLED_DEFAULT: True},
},
0xA3: { # Lighting system
0xC0: { # Set scene
CONF_ICON: "mdi:palette",
CONF_TYPE: DEVICE_CLASS_ECHONETLITE_LIGHT_SCENE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_MAXIMUM: 0xFD,
CONF_MAX_OPC: 0xC1,
},
},
},
0xA5: { # Multiple Input PCS
0xE0: { # Measured cumulative amount of electric energy (normal direction)
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
CONF_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
CONF_MULTIPLIER: 0.001,
},
0xE3: { # Measured cumulative amount of electric energy (reverse direction)
CONF_TYPE: SensorDeviceClass.ENERGY,
CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
CONF_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
CONF_MULTIPLIER: 0.001,
},
0xE7: { # Measured instantaneous amount of electricity
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
},
},
0xA6: { # Hybrid Water Heater
0xE1: { # Measured amount of hot water remaining in tank
CONF_TYPE: SensorDeviceClass.WATER,
CONF_UNIT_OF_MEASUREMENT: UnitOfVolume.LITERS,
},
0xE2: { # Tank Capacity
CONF_TYPE: SensorDeviceClass.WATER,
CONF_UNIT_OF_MEASUREMENT: UnitOfVolume.LITERS,
},
},
},
0x03: { # Cooking/housework-related device class group
0xB7: { # Refrigerator
0xB0: {
CONF_ICON: "mdi:door",
CONF_ICONS: {
DATA_STATE_OPEN: "mdi:door-open",
DATA_STATE_CLOSE: "mdi:door-closed",
},
}, # "Door open/close status",
0xB1: {
CONF_ICON: "mdi:door",
CONF_ICONS: {
DATA_STATE_OPEN: "mdi:door-open",
DATA_STATE_CLOSE: "mdi:door-closed",
},
}, # "Door open warning",
0xB2: {
CONF_ICON: "mdi:door",
CONF_ICONS: {
DATA_STATE_OPEN: "mdi:door-open",
DATA_STATE_CLOSE: "mdi:door-closed",
},
}, # "Refrigerator compartment door status",
0xB3: {
CONF_ICON: "mdi:door",
CONF_ICONS: {
DATA_STATE_OPEN: "mdi:door-open",
DATA_STATE_CLOSE: "mdi:door-closed",
},
}, # "Freezer compartment door status",
0xB4: {
CONF_ICON: "mdi:door",
CONF_ICONS: {
DATA_STATE_OPEN: "mdi:door-open",
DATA_STATE_CLOSE: "mdi:door-closed",
},
}, # "Ice compartment door status",
0xB5: {
CONF_ICON: "mdi:door",
CONF_ICONS: {
DATA_STATE_OPEN: "mdi:door-open",
DATA_STATE_CLOSE: "mdi:door-closed",
},
}, # "Vegetable compartment door status",
0xB6: {
CONF_ICON: "mdi:door",
CONF_ICONS: {
DATA_STATE_OPEN: "mdi:door-open",
DATA_STATE_CLOSE: "mdi:door-closed",
},
}, # "Multi-refrigera-ting mode compartment door",
0xE0: {
TYPE_DATA_DICT: [
"refrigerator",
"freezer",
"ice",
"vegetable",
"multi_refrigerating",
],
CONF_DISABLED_DEFAULT: True,
}, # "Maximum allowable temperature setting level",
0xE2: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.TEMPERATURE,
CONF_MINIMUM: -127,
CONF_MAXIMUM: 126,
},
}, # "Refrigerator compartment temperature setting",
0xE3: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.TEMPERATURE,
CONF_MINIMUM: -127,
CONF_MAXIMUM: 126,
},
}, # "Freezer compartment temperature setting",
0xE4: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.TEMPERATURE,
CONF_MINIMUM: -127,
CONF_MAXIMUM: 126,
},
}, # "Ice temperature setting",
0xE5: {
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.TEMPERATURE,
CONF_MINIMUM: -127,
CONF_MAXIMUM: 126,
},
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
}, # "Vegetable compartment temperature setting",
0xE6: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.TEMPERATURE,
CONF_MINIMUM: -127,
CONF_MAXIMUM: 126,
},
}, # "Multi-refrigera-ting mode compartment temperature setting",
0xE9: {
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.TEMPERATURE,
CONF_MINIMUM: 1,
CONF_MAXIMUM: 0xFF,
CONF_MAX_OPC: [0xE0, "refrigerator"],
},
}, # "Refrigerator compartment temperature level setting",
0xEA: {
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.TEMPERATURE,
CONF_MINIMUM: 1,
CONF_MAXIMUM: 0xFF,
CONF_MAX_OPC: [0xE0, "freezer"],
},
}, # "Freezer compartment temperature level setting",
0xEB: {
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.TEMPERATURE,
CONF_MINIMUM: 1,
CONF_MAXIMUM: 0xFF,
CONF_MAX_OPC: [0xE0, "ice"],
},
}, # "ice compartment temperature level setting",
0xEC: {
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.TEMPERATURE,
CONF_MINIMUM: 1,
CONF_MAXIMUM: 0xFF,
CONF_MAX_OPC: [0xE0, "vegetable"],
},
}, # "Vegetable compartment temperature level setting",
0xED: {
TYPE_NUMBER: {
CONF_TYPE: NumberDeviceClass.TEMPERATURE,
CONF_MINIMUM: 1,
CONF_MAXIMUM: 0xFF,
CONF_MAX_OPC: [0xE0, "multi_refrigerating"],
},
}, # "Multi-refrigera-ting mode compartment temperature level setting",
0xD1: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
}, # "Measured refrigerator compartment temperature",
0xD2: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
}, # "Measured freezer compartment temperature",
0xD3: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
}, # "Measured subzero-fresh compartment temperature",
0xD4: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
}, # "Measured vegetable compartment temperature",
0xD5: {
CONF_TYPE: SensorDeviceClass.TEMPERATURE,
}, # "Measured multi-refrigeratin g mode compartment temperature",
0xD8: {
TYPE_DATA_DICT: ["maximum_rotation_speed", "rotation_speed"]
}, # "Compressor rotation speed",
0xDA: {
CONF_TYPE: SensorDeviceClass.CURRENT,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
CONF_MULTIPLIER: 0.1,
}, # "Measured electric current consumption",
0xDC: {
CONF_TYPE: SensorDeviceClass.POWER,
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
}, # "Rated power consumption",
0xA0: {
CONF_ICON: "mdi:snowflake-check"
}, # "Quick freeze function setting",
0xA1: {
CONF_ICON: "mdi:fridge-bottom"
}, # "Quick refrigeration function setting",
0xA4: {CONF_ICON: "mdi:dice-1-outline"}, # "Icemaker setting",
0xA5: {CONF_ICON: "mdi:dice-1-outline"}, # "Icemaker operation status",
0xA6: {CONF_ICON: "mdi:water-alert-outline"}, # "Icemaker tank status",
0xA8: {
CONF_ICON: "mdi:water-thermometer"
}, # "Refrigerator compartment humidification function setting",
0xA9: {
CONF_ICON: "mdi:water-thermometer"
}, # "Vegetable compartment humidification function setting",
0xAD: {CONF_ICON: "mdi:scent"}, # "Deodorization function setting",
}, # Refrigerator
},
"default": {
CONF_ICON: None,
CONF_TYPE: None,
CONF_STATE_CLASS: None,
},
}
ENABLE_SUPER_ENERGY_DEFAULT = {
# If False is not specified here, the default is True.
# 0x01: {
# 0x35: False,
# },
}
# Some entities that overlap with control entities are excluded from setup
NON_SETUP_SINGLE_ENYITY = {
0x01: {
# Home Air Conditioner
0x30: {ENL_HVAC_MODE, ENL_HVAC_SET_TEMP, ENL_HVAC_SILENT_MODE},
# Ceiling fan
0x3A: {
ENL_FANSPEED_PERCENT,
ENL_FAN_DIRECTION,
ENL_FAN_OSCILLATION,
ENL_FAN_LIGHT_STATUS,
ENL_FAN_LIGHT_MODE,
ENL_FAN_LIGHT_BRIGHTNESS,
ENL_FAN_LIGHT_COLOR_TEMP,
ENL_FAN_LIGHT_NIGHT_BRIGHTNESS,
ENL_BUZZER,
},
},
0x02: {
0x60: {ENL_OPENING_LEVEL, ENL_BLIND_ANGLE, ENL_OPENCLOSE_STATUS},
0x61: {ENL_OPENING_LEVEL, ENL_BLIND_ANGLE, ENL_OPENCLOSE_STATUS},
0x62: {ENL_OPENING_LEVEL, ENL_BLIND_ANGLE, ENL_OPENCLOSE_STATUS},
0x63: {ENL_OPENING_LEVEL, ENL_BLIND_ANGLE, ENL_OPENCLOSE_STATUS},
0x64: {ENL_OPENING_LEVEL, ENL_BLIND_ANGLE, ENL_OPENCLOSE_STATUS},
0x65: {ENL_OPENING_LEVEL, ENL_BLIND_ANGLE, ENL_OPENCLOSE_STATUS},
0x66: {ENL_OPENING_LEVEL, ENL_BLIND_ANGLE, ENL_OPENCLOSE_STATUS},
# General Lighting
0x90: {ENL_BRIGHTNESS, ENL_COLOR_TEMP},
# Single Function Lighting
0x91: {ENL_BRIGHTNESS, ENL_COLOR_TEMP},
},
}
ATTR_STATE_ON = "on"
ATTR_STATE_OFF = "off"
FAN_SPEED_OPTIONS = {
"auto": "Auto",
"minimum": "Minimum",
"low": "Low",
"medium-low": "Medium-Low",
"medium": "Medium",
"medium-high": "Medium-High",
"high": "High",
"very-high": "Very-High",
"max": "Max",
}
AIRFLOW_HORIZ_OPTIONS = {
"rc-right": "Right Center + Right",
"left-lc": "Left + Left Center",
"lc-center-rc": "Left + Center + Right Center",
"left-lc-rc-right": "Left + Left Center + Right Center + Right",
"right": "Right",
"rc": "Right Center",
"center": "Center",
"center-right": "Center + Right",
"center-rc": "Center + Right Center",
"center-rc-right": "Center + Right Center + Right",
"lc": "Left Center",
"lc-right": "Left Center + Right",
"lc-rc": "Left Center + Right Center",
"lc-rc-right": "Left Center + Right Center + Right",
"lc-center": "Left Center + Center",
"lc-center-right": "Left Center + Center + Right",
"lc-center-rc-right": "Left Center + Center + Right Center + Right",
"left": "Left",
"left-right": "Left + Right",
"left-rc": "Left + Right Center",
"left-rc-right": "Left + Right Center + Right",
"left-center": "Left + Center",
"left-center-right": "Left + Center + Right",
"left-center-rc": "Left + Center + Right Center",
"left-center-rc-right": "Left + Center + Right Center + Right",
"left-lc-right": "Left + Left Center + Right",
"left-lc-rc": "Left + Left Center + Right Center",
"left-lc-center": "Left + Left Center + Center",
"left-lc-center-right": "Left + Left Center + Center + Right",
"left-lc-center-rc": "Left + Left Center + Center + Right Center",
"left-lc-center-rc-right": "Left + Left Center + Center + Right Center + Right",
}
AIRFLOW_VERT_OPTIONS = {
"upper": "Upper",
"upper-central": "Upper Central",
"central": "Central",
"lower-central": "Lower Central",
"lower": "Lower",
}
AUTO_DIRECTION_OPTIONS = {
"auto": "Auto",
"non-auto": "Non-Auto",
"auto-vert": "Auto-vert",
"auto-horiz": "Auto-horiz",
}
SWING_MODE_OPTIONS = {
"not-used": "Not Used (Off)",
"vert": "Vertical",
"horiz": "Horizontal",
"vert-horiz": "Vertical-Horizontal",
}
SILENT_MODE_OPTIONS = {
"normal": "Normal",
"high-speed": "High Speed",
"silent": "Silent",
}
HVAC_MODE_OPTIONS = {"as_off": "As Off", "as_idle": "As Idle"}
OPTION_HA_UI_SWING = "ha_ui_swing"
USER_OPTIONS = {
ENL_FANSPEED: {"option": "fan_settings", "option_list": FAN_SPEED_OPTIONS},
ENL_SWING_MODE: {"option": "swing_mode", "option_list": SWING_MODE_OPTIONS},
ENL_AUTO_DIRECTION: {
"option": "auto_direction",
"option_list": AUTO_DIRECTION_OPTIONS,
},
ENL_AIR_VERT: {"option": "swing_vert", "option_list": AIRFLOW_VERT_OPTIONS},
ENL_AIR_HORZ: {"option": "swing_horiz", "option_list": AIRFLOW_HORIZ_OPTIONS},
ENL_HVAC_MODE: {
"option": CONF_OTHER_MODE,
"option_list": [
{"value": "as_off", "label": "As Off"},
{"value": "as_idle", "label": "As Idle"},
],
},
OPTION_HA_UI_SWING: {"option": OPTION_HA_UI_SWING, "option_list": []},
}
TEMP_OPTIONS = {
"min_temp_heat": {"min": 10, "max": 25, "default": 16},
"max_temp_heat": {"min": 18, "max": 30, "default": 30},
"min_temp_cool": {"min": 15, "max": 25, "default": 16},
"max_temp_cool": {"min": 18, "max": 30, "default": 30},
"min_temp_auto": {"min": 15, "max": 25, "default": 16},
"max_temp_auto": {"min": 18, "max": 30, "default": 30},
}
MISC_OPTIONS = {
CONF_FORCE_POLLING: {"type": bool, "default": False},
CONF_ENABLE_SUPER_ENERGY: {
"type": bool,
"default": [ENABLE_SUPER_ENERGY_DEFAULT, True],
},
CONF_BATCH_SIZE_MAX: {"type": int, "default": 10, "min": 1, "max": 30},
}
================================================
FILE: custom_components/echonetlite/cover.py
================================================
import logging
import math
from typing import Any
from pychonet.lib.epc_functions import (
DATA_STATE_CLOSE,
DATA_STATE_OPEN,
DATA_STATE_STOP,
DATA_STATE_OPENING,
DATA_STATE_CLOSING,
)
from . import get_device_name
from .const import DOMAIN
from homeassistant.components.cover import (
ATTR_POSITION,
ATTR_TILT_POSITION,
CoverEntity,
CoverEntityFeature,
)
from pychonet.lib.eojx import EOJX_CLASS
from pychonet.ElectricBlind import (
ENL_BLIND_ANGLE,
ENL_OPENCLOSE_STATUS,
ENL_OPENING_LEVEL,
ENL_OPENSTATE,
)
from homeassistant.util.percentage import (
percentage_to_ranged_value,
ranged_value_to_percentage,
)
TILT_RANGE = (1, 180)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up entry."""
entities = []
for entity in hass.data[DOMAIN][config_entry.entry_id]:
if entity["instance"]["eojgc"] == 0x02 and entity["instance"]["eojcc"] in (
0x60,
0x61,
0x62,
0x63,
0x64,
0x65,
0x66,
):
# 0x60: "Electrically operated blind/shade"
# 0x61: "Electrically operated shutter"
# 0x62: "Electrically operated curtain"
# 0x63: "Electrically operated rain sliding door/shutter"
# 0x64: "Electrically operated gate"
# 0x65: "Electrically operated window"
# 0x66: "Automatically operated entrance door/sliding door"
entities.append(EchonetCover(entity["echonetlite"], config_entry))
async_add_devices(entities, True)
class EchonetCover(CoverEntity):
"""Representation of an ECHONETLite climate device."""
def __init__(self, connector, config):
"""Initialize the cover device."""
name = get_device_name(connector, config)
self._attr_name = name
self._device_name = name
self._connector = connector # new line
self._attr_unique_id = (
self._connector._uidi if self._connector._uidi else self._connector._uid
)
self._attr_is_closed = False
self._server_state = self._connector._api._state[
self._connector._instance._host
]
self._olddata = {}
self._attr_should_poll = True
self._support_flags = (
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP
)
if ENL_OPENING_LEVEL in list(self._connector._setPropertyMap):
self._support_flags |= CoverEntityFeature.SET_POSITION
if ENL_BLIND_ANGLE in list(self._connector._setPropertyMap):
self._support_flags |= (
CoverEntityFeature.OPEN_TILT
| CoverEntityFeature.CLOSE_TILT
# not supported individually (just global STOP)
# | CoverEntityFeature.STOP_TILT
| CoverEntityFeature.SET_TILT_POSITION
)
self._attr_current_cover_position = None
self._attr_current_cover_tilt_position = None
self._attr_is_opening = False
self._attr_is_closing = False
self.update_attr()
self.update_option_listener()
async def async_close_cover(self, **kwargs: Any) -> None:
await self._connector._instance.setMessage(ENL_OPENSTATE, 0x42)
self._connector._update_data[ENL_OPENSTATE] = DATA_STATE_CLOSE
if ENL_OPENCLOSE_STATUS in self._connector._update_data:
self._attr_is_opening = False
self._attr_is_closing = True
async def async_open_cover(self, **kwargs: Any) -> None:
await self._connector._instance.setMessage(ENL_OPENSTATE, 0x41)
self._connector._update_data[ENL_OPENSTATE] = DATA_STATE_OPEN
if ENL_OPENCLOSE_STATUS in self._connector._update_data:
self._attr_is_opening = True
self._attr_is_closing = False
async def async_stop_cover(self, **kwargs: Any) -> None:
await self._connector._instance.setMessage(ENL_OPENSTATE, 0x43)
self._connector._update_data[ENL_OPENSTATE] = DATA_STATE_STOP
self._attr_is_opening = False
self._attr_is_closing = False
await self._connector.async_update()
self.update_attr()
async def async_set_cover_position(self, **kwargs: Any) -> None:
desired_position = kwargs[ATTR_POSITION]
current_position = self._attr_current_cover_position
await self._connector._instance.setMessage(ENL_OPENING_LEVEL, desired_position)
self._connector._update_data[ENL_OPENING_LEVEL] = int(desired_position)
self._attr_is_opening = desired_position > current_position
self._attr_is_closing = desired_position < current_position
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
await self._connector._instance.setMessage(ENL_BLIND_ANGLE, 0)
self._connector._update_data[ENL_BLIND_ANGLE] = 0
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
await self._connector._instance.setMessage(ENL_BLIND_ANGLE, 180)
self._connector._update_data[ENL_BLIND_ANGLE] = 180
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
tilt = math.ceil(
percentage_to_ranged_value(TILT_RANGE, kwargs[ATTR_TILT_POSITION])
)
await self._connector._instance.setMessage(ENL_BLIND_ANGLE, tilt)
self._connector._update_data[ENL_BLIND_ANGLE] = int(tilt)
async def async_update(self):
await self._connector.async_update()
def update_attr(self):
if (
ENL_OPENING_LEVEL in self._connector._update_data
and self._connector._update_data[ENL_OPENING_LEVEL] != None
):
self._attr_current_cover_position = int(
self._connector._update_data[ENL_OPENING_LEVEL]
)
self._attr_is_closed = self._attr_current_cover_position == 0
else:
self._attr_is_closed = (
self._connector._update_data[ENL_OPENSTATE] == DATA_STATE_CLOSE
)
if ENL_OPENCLOSE_STATUS in self._connector._update_data:
self._attr_is_opening = (
self._connector._update_data[ENL_OPENCLOSE_STATUS] == DATA_STATE_OPENING
)
self._attr_is_closing = (
self._connector._update_data[ENL_OPENCLOSE_STATUS] == DATA_STATE_CLOSING
)
if (
ENL_BLIND_ANGLE in self._connector._update_data
and self._connector._update_data[ENL_BLIND_ANGLE] != None
):
self._attr_current_cover_tilt_position = ranged_value_to_percentage(
TILT_RANGE, int(self._connector._update_data[ENL_BLIND_ANGLE])
)
@property
def device_info(self):
return {
"identifiers": {
(
DOMAIN,
self._connector._uid,
self._connector._instance._eojgc,
self._connector._instance._eojcc,
self._connector._instance._eojci,
)
},
"name": self._device_name,
"manufacturer": self._connector._manufacturer
+ (
" " + self._connector._host_product_code
if self._connector._host_product_code
else ""
),
"model": EOJX_CLASS[self._connector._instance._eojgc][
self._connector._instance._eojcc
],
# "sw_version": "",
}
async def async_added_to_hass(self):
"""Register callbacks."""
self._connector.add_update_option_listener(self.update_option_listener)
self._connector.register_async_update_callbacks(self.async_update_callback)
async def async_update_callback(self, isPush=False):
changed = (
self._olddata != self._connector._update_data
) or self._attr_available != self._server_state["available"]
if changed:
self._olddata = self._connector._update_data.copy()
self.update_attr()
if self._attr_available != self._server_state["available"]:
if self._server_state["available"]:
self.update_option_listener()
else:
self._attr_should_poll = True
self._attr_available = self._server_state["available"]
self.async_schedule_update_ha_state()
def update_option_listener(self):
_LOGGER.info(f"{self._device_name}: _should_poll is {self._attr_should_poll}")
================================================
FILE: custom_components/echonetlite/fan.py
================================================
import logging
from pychonet.HomeAirCleaner import FAN_SPEED
from pychonet.lib.const import ENL_STATUS
from pychonet.lib.eojx import EOJX_CLASS
from pychonet.CeilingFan import (
ENL_FANSPEED_PERCENT,
ENL_FAN_DIRECTION,
ENL_FAN_OSCILLATION,
)
from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.const import (
PRECISION_WHOLE,
)
from . import get_device_name
from .const import (
CONF_FORCE_POLLING,
DATA_STATE_ON,
DOMAIN,
ENL_FANSPEED,
)
_LOGGER = logging.getLogger(__name__)
DEFAULT_FAN_MODES = list(
FAN_SPEED.keys()
) # ["auto","minimum","low","medium-low","medium","medium-high","high","very-high","max"]
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up entry."""
entities = []
for entity in hass.data[DOMAIN][config_entry.entry_id]:
if entity["instance"]["eojgc"] == 0x01 and (
entity["instance"]["eojcc"] == 0x35 or entity["instance"]["eojcc"] == 0x3A
): # Home Air Cleaner or Celing Fan
entities.append(EchonetFan(entity["echonetlite"], config_entry))
async_add_devices(entities, True)
class EchonetFan(FanEntity):
"""Representation of an ECHONETLite Fan device (eg Air purifier)."""
def __init__(self, connector, config):
"""Initialize the climate device."""
name = get_device_name(connector, config)
self._attr_name = name
self._device_name = name
self._connector = connector # new line
self._attr_unique_id = (
self._connector._uidi if self._connector._uidi else self._connector._uid
)
self._precision = 1.0
self._target_temperature_step = 1
self._server_state = self._connector._api._state[
self._connector._instance._host
]
self._attr_supported_features = FanEntityFeature(0)
if hasattr(FanEntityFeature, "TURN_ON"): # v2024.8
self._attr_supported_features |= FanEntityFeature.TURN_ON
if hasattr(FanEntityFeature, "TURN_OFF"):
self._attr_supported_features |= FanEntityFeature.TURN_OFF
if ENL_FANSPEED in list(self._connector._setPropertyMap):
self._attr_supported_features |= FanEntityFeature.PRESET_MODE
if ENL_FANSPEED_PERCENT in list(self._connector._setPropertyMap):
self._attr_supported_features |= FanEntityFeature.SET_SPEED
if ENL_FAN_DIRECTION in list(self._connector._setPropertyMap):
self._attr_supported_features |= FanEntityFeature.DIRECTION
if ENL_FAN_OSCILLATION in list(self._connector._setPropertyMap):
self._attr_supported_features |= FanEntityFeature.OSCILLATE
self._olddata = {}
self._attr_should_poll = True
self._attr_available = True
self._attr_speed_count = getattr(self._connector._instance, "SPEED_COUNT", 100)
self._set_attrs()
self.update_option_listener()
# see, https://developers.home-assistant.io/blog/2024/07/19/fan-fanentityfeatures-turn-on_off/
self._enable_turn_on_off_backwards_compatibility = False
async def async_update(self):
try:
await self._connector.async_update()
except TimeoutError:
pass
@property
def device_info(self):
return {
"identifiers": {
(
DOMAIN,
self._connector._uid,
self._connector._instance._eojgc,
self._connector._instance._eojcc,
self._connector._instance._eojci,
)
},
"name": self._device_name,
"manufacturer": self._connector._manufacturer
+ (
" " + self._connector._host_product_code
if self._connector._host_product_code
else ""
),
"model": EOJX_CLASS[self._connector._instance._eojgc][
self._connector._instance._eojcc
],
# "sw_version": "",
}
@property
def precision(self) -> float:
return PRECISION_WHOLE
@property
def is_on(self):
"""Return true if the device is on."""
return (
True if self._connector._update_data[ENL_STATUS] == DATA_STATE_ON else False
)
def _set_attrs(self):
# @property
# def preset_mode(self):
"""Return the fan setting."""
self._attr_preset_mode = (
self._connector._update_data[ENL_FANSPEED]
if ENL_FANSPEED in self._connector._update_data
else None
)
# @property
# def percentage(self):
"""Return the fan setting."""
self._attr_percentage = (
self._connector._update_data[ENL_FANSPEED_PERCENT]
if ENL_FANSPEED_PERCENT in self._connector._update_data
else None
)
# @property
# def current_direction(self):
"""Return the fan direction."""
self._attr_current_direction = (
self._connector._update_data[ENL_FAN_DIRECTION]
if ENL_FAN_DIRECTION in self._connector._update_data
else None
)
# @property
# def oscillating(self):
"""Return the fan oscillating."""
self._attr_oscillating = (
self._connector._update_data[ENL_FAN_OSCILLATION]
if ENL_FAN_OSCILLATION in self._connector._update_data
else None
)
# @property
# def preset_modes(self):
"""Return the list of available fan modes."""
if (
ENL_FANSPEED in list(self._connector._user_options.keys())
and self._connector._user_options[ENL_FANSPEED] is not False
):
self._attr_preset_modes = self._connector._user_options[ENL_FANSPEED]
else:
self._attr_preset_modes = DEFAULT_FAN_MODES
async def async_set_direction(self, direction: str) -> None:
await self._connector._instance.setFanDirection(direction)
async def async_turn_on(
self,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs,
) -> None:
"""Turn on."""
await self._connector._instance.on()
async def async_turn_off(self, **kwargs):
"""Turn off."""
await self._connector._instance.off()
async def async_oscillate(self, oscillating: bool) -> None:
await self._connector._instance.setFanOscillation(oscillating)
async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed percentage of the fan."""
await self._connector._instance.setFanSpeedPercent(percentage)
async def async_set_preset_mode(self, preset_mode: str):
"""Set new fan mode."""
await self._connector._instance.setFanSpeed(preset_mode)
async def async_added_to_hass(self):
"""Register callbacks."""
self._connector.register_async_update_callbacks(self.async_update_callback)
self._connector.add_update_option_listener(self.update_option_listener)
async def async_update_callback(self, isPush: bool = False):
changed = (
self._olddata != self._connector._update_data
or self._attr_available != self._server_state["available"]
)
if changed:
_force = bool(not self._attr_available and self._server_state["available"])
self._olddata = self._connector._update_data.copy()
if self._attr_available != self._server_state["available"]:
if self._server_state["available"]:
self.update_option_listener()
else:
self._attr_should_poll = True
self._attr_available = self._server_state["available"]
self._set_attrs()
self.async_schedule_update_ha_state(_force | isPush)
def update_option_listener(self):
_should_poll = (
ENL_STATUS not in self._connector._ntfPropertyMap
or (
FanEntityFeature.PRESET_MODE in self._attr_supported_features
and ENL_FANSPEED not in self._connector._ntfPropertyMap
)
or (
FanEntityFeature.SET_SPEED in self._attr_supported_features
and ENL_FANSPEED_PERCENT not in self._connector._ntfPropertyMap
)
or (
FanEntityFeature.DIRECTION in self._attr_supported_features
and ENL_FAN_DIRECTION not in self._connector._ntfPropertyMap
)
or (
FanEntityFeature.OSCILLATE in self._attr_supported_features
and ENL_FAN_OSCILLATION not in self._connector._ntfPropertyMap
)
)
self._attr_should_poll = (
self._connector._user_options.get(CONF_FORCE_POLLING, False) or _should_poll
)
self._attr_extra_state_attributes = {"notify": "No" if _should_poll else "Yes"}
_LOGGER.debug(f"{self._attr_name}: _should_poll is {_should_poll}")
================================================
FILE: custom_components/echonetlite/light.py
================================================
import logging
from pychonet.GeneralLighting import ENL_STATUS, ENL_BRIGHTNESS, ENL_COLOR_TEMP
from pychonet.CeilingFan import (
ENL_FAN_LIGHT_STATUS,
ENL_FAN_LIGHT_BRIGHTNESS,
ENL_FAN_LIGHT_COLOR_TEMP,
)
from pychonet.lib.const import ENL_ON
from pychonet.lib.eojx import EOJX_CLASS
from pychonet.lib.epc_functions import _swap_dict
from homeassistant.components.light import (
ATTR_EFFECT,
LightEntity,
ColorMode,
LightEntityFeature,
)
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN,
)
from . import get_device_name
from .const import DATA_STATE_ON, DOMAIN, CONF_FORCE_POLLING
_LOGGER = logging.getLogger(__name__)
DEFAULT_BRIGHTNESS_SCALE = 255
MIN_MIREDS = 153 # 6500k
MAX_MIREDS = 500 # 2000k
DEVICE_SCALE = 100
def _mireds_to_kelvin(mireds):
"""Convert mireds to kelvin."""
return round(1000000 / mireds) if mireds else None
def _kelvin_to_mireds(kelvin):
"""Convert kelvin to mireds."""
return round(1000000 / kelvin) if kelvin else None
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up entry."""
entities = []
for entity in hass.data[DOMAIN][config_entry.entry_id]:
eojgc = entity["instance"]["eojgc"]
eojcc = entity["instance"]["eojcc"]
if (eojgc == 0x02 and eojcc in (0x90, 0x91, 0xA3)) or (
eojgc == 0x01
and eojcc == 0x3A
and ENL_FAN_LIGHT_STATUS in entity["echonetlite"]._setPropertyMap
):
custom_options = {}
# General Lighting (0x90), Mono Functional Lighting (0x91), Lighting System (0xA3)
if eojgc == 0x02 and eojcc in (0x90, 0x91, 0xA3):
custom_options = {
ENL_STATUS: ENL_STATUS,
ENL_BRIGHTNESS: ENL_BRIGHTNESS,
ENL_COLOR_TEMP: ENL_COLOR_TEMP,
"echonet_color": {
0x44: "daylight_color",
0x43: "daylight_white",
0x42: "white",
0x40: "other",
0x41: "incandescent_lamp_color",
},
"echonet_mireds_int": {
0x44: 153, # 6500K
0x43: 200, # 5000K
0x42: 238, # 4200K
0x40: 285, # 3500K
0x41: 370, # 2700K
}, # coolest to warmest value is mired
"on": "on",
"off": "off",
}
custom_options["echonet_int_color"] = _swap_dict(
custom_options["echonet_color"]
)
# Ceiling Fan (0x01-0x3A)
elif eojgc == 0x01 and eojcc == 0x3A:
custom_options = {
ENL_STATUS: ENL_FAN_LIGHT_STATUS,
ENL_BRIGHTNESS: ENL_FAN_LIGHT_BRIGHTNESS,
ENL_COLOR_TEMP: ENL_FAN_LIGHT_COLOR_TEMP,
"echonet_color": None,
"echonet_mireds_int": None,
"on": "light_on",
"off": "light_off",
}
_LOGGER.debug("Configuring ECHONETlite Light entity")
entities.append(
EchonetLight(
entity["echonetlite"],
config_entry,
custom_options,
)
)
_LOGGER.debug(f"Number of light devices to be added: {len(entities)}")
async_add_devices(entities, True)
class EchonetLight(LightEntity):
"""Representation of a ECHONET light device."""
def __init__(self, connector, config, custom_options):
"""Initialize the climate device."""
name = get_device_name(connector, config)
self._attr_name = name
self._connector = connector # new line
self._attr_unique_id = (
self._connector._uidi if self._connector._uidi else self._connector._uid
)
self._attr_supported_features = LightEntityFeature(0)
self._attr_supported_color_modes = set()
self._server_state = self._connector._api._state[
self._connector._instance._host
]
if mireds_int := custom_options.get("echonet_mireds_int"):
mireds = mireds_int.values()
self._attr_min_color_temp_kelvin = _mireds_to_kelvin(max(mireds))
self._attr_max_color_temp_kelvin = _mireds_to_kelvin(min(mireds))
else:
self._attr_min_color_temp_kelvin = _mireds_to_kelvin(MAX_MIREDS)
self._attr_max_color_temp_kelvin = _mireds_to_kelvin(MIN_MIREDS)
# Keep mired limits for internal calculations
if mireds_int := custom_options.get("echonet_mireds_int"):
mireds = mireds_int.values()
self._min_mireds = min(mireds)
self._max_mireds = max(mireds)
else:
self._min_mireds = MIN_MIREDS
self._max_mireds = MAX_MIREDS
self._custom_options = custom_options
if custom_options[ENL_COLOR_TEMP] in list(self._connector._setPropertyMap):
self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP)
self._attr_color_mode = ColorMode.COLOR_TEMP
if custom_options[ENL_BRIGHTNESS] in list(self._connector._setPropertyMap):
if not self._attr_supported_color_modes:
self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS)
self._attr_color_mode = ColorMode.BRIGHTNESS
if not self._attr_supported_color_modes:
self._attr_supported_color_modes.add(ColorMode.ONOFF)
self._attr_color_mode = ColorMode.ONOFF
self._olddata = {}
self._attr_is_on = (
True
if self._connector._update_data[custom_options[ENL_STATUS]] == DATA_STATE_ON
else False
)
if hasattr(self._connector._instance, "getEffectList"):
self._attr_effect_list = self._connector._instance.getEffectList()
if self._attr_effect_list:
self._attr_supported_features |= LightEntityFeature.EFFECT
if hasattr(self._connector._instance, "getLightColorLevelMax"):
self._light_color_level_max = (
self._connector._instance.getLightColorLevelMax()
)
else:
self._light_color_level_max = 100
self._attr_should_poll = True
self._attr_available = True
self._set_attrs()
self.update_option_listener()
async def async_update(self):
"""Get the latest state from the Light."""
try:
await self._connector.async_update()
except TimeoutError:
pass
@property
def device_info(self):
return {
"identifiers": {
(
DOMAIN,
self._connector._uid,
self._connector._instance._eojgc,
self._connector._instance._eojcc,
self._connector._instance._eojci,
)
},
"name": self._attr_name,
"manufacturer": self._connector._manufacturer
+ (
" " + self._connector._host_product_code
if self._connector._host_product_code
else ""
),
"model": EOJX_CLASS[self._connector._instance._eojgc][
self._connector._instance._eojcc
],
# "sw_version": "",
}
async def async_turn_on(self, **kwargs):
states = {"status": ENL_ON}
if (
ATTR_BRIGHTNESS in kwargs
and self._attr_supported_color_modes
and self._attr_color_mode in {ColorMode.BRIGHTNESS, ColorMode.COLOR_TEMP}
):
normalized_brightness = (
float(kwargs[ATTR_BRIGHTNESS]) / DEFAULT_BRIGHTNESS_SCALE
)
device_brightness = round(normalized_brightness * DEVICE_SCALE)
# Make sure the brightness is not rounded down to 0
device_brightness = max(device_brightness, 1)
# send the message to the lamp
states["brightness"] = device_brightness
self._attr_brightness = kwargs[ATTR_BRIGHTNESS]
if (
ATTR_COLOR_TEMP_KELVIN in kwargs
and self._attr_supported_color_modes
and self._attr_color_mode == ColorMode.COLOR_TEMP
):
# Convert kelvin from HA to mireds for internal device logic
attr_color_tmp = float(_kelvin_to_mireds(kwargs[ATTR_COLOR_TEMP_KELVIN]))
if self._custom_options["echonet_color"]:
color_temp_int = 0x41
for i, mired in self._custom_options["echonet_mireds_int"].items():
if attr_color_tmp <= mired + 15:
color_temp_int = i
break
color_temp = self._custom_options["echonet_color"].get(color_temp_int)
_LOGGER.debug(
f"New color temp of light: {color_temp} - {color_temp_int}"
)
self._attr_color_temp_kelvin = _mireds_to_kelvin(
int(self._custom_options["echonet_mireds_int"].get(color_temp_int))
)
else:
color_scale = (attr_color_tmp - float(self._min_mireds)) / float(
self._max_mireds - self._min_mireds
)
_LOGGER.debug(f"Set color to : {color_scale}")
color_temp_int = min(
self._light_color_level_max,
max(1, (1 - color_scale) * self._light_color_level_max),
)
_LOGGER.debug(
f"New color temp of light: {attr_color_tmp} mireds - {color_temp_int}"
)
self._attr_color_temp_kelvin = _mireds_to_kelvin(int(attr_color_tmp))
states["color_temperature"] = int(color_temp_int)
if ATTR_EFFECT in kwargs and kwargs[ATTR_EFFECT] in self._attr_effect_list:
states[ATTR_EFFECT] = kwargs[ATTR_EFFECT]
if hasattr(self._connector._instance, "setLightStates"):
return await self._connector._instance.setLightStates(states)
else:
"""Turn on."""
result = await getattr(
self._connector._instance, self._custom_options["on"]
)()
if result:
if states.get("brightness"):
result &= await self._connector._instance.setBrightness(
states["brightness"]
)
if states.get("color_temperature"):
result &= await self._connector._instance.setColorTemperature(
states["color_temperature"]
)
async def async_turn_off(self, **kwargs):
"""Turn off."""
await getattr(self._connector._instance, self._custom_options["off"])()
def _set_attrs(self):
if self._attr_supported_color_modes and self._attr_color_mode in {
ColorMode.BRIGHTNESS,
ColorMode.COLOR_TEMP,
}:
"""brightness of this light between 0..255."""
_LOGGER.debug(
f"Current brightness of light: {self._connector._update_data[self._custom_options[ENL_BRIGHTNESS]]}"
)
brightness = (
int(self._connector._update_data[self._custom_options[ENL_BRIGHTNESS]])
if self._custom_options[ENL_BRIGHTNESS] in self._connector._update_data
else -1
)
if brightness >= 0:
self._attr_brightness = min(
round(float(brightness) / DEVICE_SCALE * DEFAULT_BRIGHTNESS_SCALE),
255,
)
else:
self._attr_brightness = 128
if (
self._attr_supported_color_modes
and self._attr_color_mode == ColorMode.COLOR_TEMP
):
"""color temperature in kelvin."""
enl_color_temp = self._custom_options[ENL_COLOR_TEMP]
gitextract_9lbvaute/ ├── .github/ │ └── workflows/ │ ├── black.yml │ └── python-app.yml ├── .gitignore ├── .vscode/ │ └── settings.json ├── LICENSE ├── README.ja.md ├── README.md ├── Services.ja.md ├── Services.md ├── custom_components/ │ └── echonetlite/ │ ├── __init__.py │ ├── binary_sensor.py │ ├── climate.py │ ├── config_flow.py │ ├── const.py │ ├── cover.py │ ├── fan.py │ ├── light.py │ ├── manifest.json │ ├── number.py │ ├── quirks/ │ │ ├── Nichicon/ │ │ │ └── all/ │ │ │ └── 02A5.py │ │ └── Panasonic/ │ │ └── all/ │ │ └── 0135.py │ ├── select.py │ ├── sensor.py │ ├── services.yaml │ ├── strings.json │ ├── switch.py │ ├── time.py │ └── translations/ │ ├── en.json │ ├── ja.json │ └── pt.json ├── hacs.json └── info.md
SYMBOL INDEX (168 symbols across 13 files)
FILE: custom_components/echonetlite/__init__.py
function _remaining_setup_budget (line 101) | def _remaining_setup_budget(started: float) -> float:
function _run_with_timeout (line 106) | async def _run_with_timeout(coro, timeout_s: float):
function get_device_name (line 112) | def get_device_name(connector, config) -> str:
function get_name_by_epc_code (line 120) | def get_name_by_epc_code(
function polling_update_debug_log (line 148) | def polling_update_debug_log(values: dict[int, Any], conn_instance: ECHO...
function get_unit_by_devise_class (line 161) | def get_unit_by_devise_class(device_class: str) -> str | None:
function regist_as_inputs (line 207) | def regist_as_inputs(epc_function_data):
function regist_as_binary_sensor (line 219) | def regist_as_binary_sensor(epc_function_data):
function async_setup_entry (line 232) | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> ...
function async_unload_entry (line 449) | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) ->...
function update_listener (line 459) | async def update_listener(hass, entry):
function get_echonet_connector (line 509) | async def get_echonet_connector():
class ECHONETConnector (line 513) | class ECHONETConnector:
method __init__ (line 517) | def __init__(self, instance, hass, entry):
method startup (line 555) | async def startup(self):
method async_update (line 614) | async def async_update(self, **kwargs):
method async_update_data (line 642) | async def async_update_data(self, kwargs):
method async_update_callback (line 659) | async def async_update_callback(self, isPush: bool = False):
method _make_update_flags_full_list (line 664) | def _make_update_flags_full_list(self):
method _make_batch_request_flags (line 700) | def _make_batch_request_flags(self):
method register_async_update_callbacks (line 722) | def register_async_update_callbacks(self, update_func):
method add_update_option_listener (line 725) | def add_update_option_listener(self, update_func):
method _load_quirk (line 728) | async def _load_quirk(self):
FILE: custom_components/echonetlite/binary_sensor.py
function async_setup_entry (line 65) | async def async_setup_entry(hass, config, async_add_entities, discovery_...
class EchonetBinarySensor (line 217) | class EchonetBinarySensor(BinarySensorEntity):
method __init__ (line 222) | def __init__(self, connector, config, op_code, attributes, hass=None) ...
method device_info (line 274) | def device_info(self):
method get_attr_is_on (line 296) | def get_attr_is_on(self):
method async_update (line 330) | async def async_update(self):
method async_added_to_hass (line 338) | async def async_added_to_hass(self):
method async_update_callback (line 343) | async def async_update_callback(self, isPush: bool = False):
method update_option_listener (line 369) | def update_option_listener(self):
FILE: custom_components/echonetlite/climate.py
function async_setup_entry (line 64) | async def async_setup_entry(hass, config_entry, async_add_devices):
class EchonetClimate (line 86) | class EchonetClimate(ClimateEntity):
method __init__ (line 91) | def __init__(self, connector, config):
method async_update (line 155) | async def async_update(self):
method device_info (line 163) | def device_info(self):
method _set_min_max_temp (line 187) | def _set_min_max_temp(self):
method _set_attrs (line 200) | def _set_attrs(self):
method async_set_fan_mode (line 306) | async def async_set_fan_mode(self, fan_mode):
method async_set_preset_mode (line 311) | async def async_set_preset_mode(self, preset_mode):
method async_set_swing_mode (line 315) | async def async_set_swing_mode(self, swing_mode):
method async_set_temperature (line 324) | async def async_set_temperature(self, **kwargs):
method async_set_humidity (line 335) | async def async_set_humidity(self, humidity: int) -> None:
method async_set_hvac_mode (line 338) | async def async_set_hvac_mode(self, hvac_mode):
method async_turn_on (line 346) | async def async_turn_on(self):
method async_turn_off (line 350) | async def async_turn_off(self):
method async_set_humidifier_during_heater (line 354) | async def async_set_humidifier_during_heater(self, state, humidity):
method async_added_to_hass (line 358) | async def async_added_to_hass(self):
method async_update_callback (line 363) | async def async_update_callback(self, isPush: bool = False):
method update_option_listener (line 378) | def update_option_listener(self):
method _normalize_settemp (line 399) | def _normalize_settemp(self, req: float | int | None) -> int | None:
FILE: custom_components/echonetlite/config_flow.py
function enumerate_instances (line 64) | async def enumerate_instances(
function async_discover_newhost (line 230) | async def async_discover_newhost(hass, host):
class ConfigFlow (line 247) | class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
method init_discover (line 257) | async def init_discover(self):
method async_step_user (line 292) | async def async_step_user(
method async_step_user_man (line 342) | async def async_step_user_man(
method async_step_finish (line 347) | async def async_step_finish(self, user_input=None):
method async_get_options_flow (line 358) | def async_get_options_flow(config_entry):
class ErrorConnect (line 362) | class ErrorConnect(HomeAssistantError):
class ErrorIpChanged (line 366) | class ErrorIpChanged(HomeAssistantError):
class OptionsFlowHandler (line 370) | class OptionsFlowHandler(config_entries.OptionsFlow):
method __init__ (line 371) | def __init__(self, config):
method async_step_init (line 375) | async def async_step_init(self, user_input=None):
method async_step_misc (line 554) | async def async_step_misc(self, user_input=None):
FILE: custom_components/echonetlite/cover.py
function async_setup_entry (line 40) | async def async_setup_entry(hass, config_entry, async_add_devices):
class EchonetCover (line 64) | class EchonetCover(CoverEntity):
method __init__ (line 67) | def __init__(self, connector, config):
method async_close_cover (line 104) | async def async_close_cover(self, **kwargs: Any) -> None:
method async_open_cover (line 111) | async def async_open_cover(self, **kwargs: Any) -> None:
method async_stop_cover (line 118) | async def async_stop_cover(self, **kwargs: Any) -> None:
method async_set_cover_position (line 126) | async def async_set_cover_position(self, **kwargs: Any) -> None:
method async_close_cover_tilt (line 134) | async def async_close_cover_tilt(self, **kwargs: Any) -> None:
method async_open_cover_tilt (line 138) | async def async_open_cover_tilt(self, **kwargs: Any) -> None:
method async_set_cover_tilt_position (line 142) | async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
method async_update (line 149) | async def async_update(self):
method update_attr (line 152) | def update_attr(self):
method device_info (line 181) | def device_info(self):
method async_added_to_hass (line 205) | async def async_added_to_hass(self):
method async_update_callback (line 210) | async def async_update_callback(self, isPush=False):
method update_option_listener (line 225) | def update_option_listener(self):
FILE: custom_components/echonetlite/fan.py
function async_setup_entry (line 30) | async def async_setup_entry(hass, config_entry, async_add_devices):
class EchonetFan (line 41) | class EchonetFan(FanEntity):
method __init__ (line 44) | def __init__(self, connector, config):
method async_update (line 84) | async def async_update(self):
method device_info (line 91) | def device_info(self):
method precision (line 116) | def precision(self) -> float:
method is_on (line 120) | def is_on(self):
method _set_attrs (line 126) | def _set_attrs(self):
method async_set_direction (line 174) | async def async_set_direction(self, direction: str) -> None:
method async_turn_on (line 177) | async def async_turn_on(
method async_turn_off (line 186) | async def async_turn_off(self, **kwargs):
method async_oscillate (line 190) | async def async_oscillate(self, oscillating: bool) -> None:
method async_set_percentage (line 193) | async def async_set_percentage(self, percentage: int) -> None:
method async_set_preset_mode (line 197) | async def async_set_preset_mode(self, preset_mode: str):
method async_added_to_hass (line 201) | async def async_added_to_hass(self):
method async_update_callback (line 206) | async def async_update_callback(self, isPush: bool = False):
method update_option_listener (line 223) | def update_option_listener(self):
FILE: custom_components/echonetlite/light.py
function _mireds_to_kelvin (line 37) | def _mireds_to_kelvin(mireds):
function _kelvin_to_mireds (line 42) | def _kelvin_to_mireds(kelvin):
function async_setup_entry (line 47) | async def async_setup_entry(hass, config_entry, async_add_devices):
class EchonetLight (line 108) | class EchonetLight(LightEntity):
method __init__ (line 111) | def __init__(self, connector, config, custom_options):
method async_update (line 177) | async def async_update(self):
method device_info (line 185) | def device_info(self):
method async_turn_on (line 209) | async def async_turn_on(self, **kwargs):
method async_turn_off (line 286) | async def async_turn_off(self, **kwargs):
method _set_attrs (line 290) | def _set_attrs(self):
method async_added_to_hass (line 346) | async def async_added_to_hass(self):
method async_update_callback (line 351) | async def async_update_callback(self, isPush: bool = False):
method update_option_listener (line 379) | def update_option_listener(self):
FILE: custom_components/echonetlite/number.py
function async_setup_entry (line 28) | async def async_setup_entry(hass, config, async_add_entities, discovery_...
class EchonetNumber (line 53) | class EchonetNumber(NumberEntity):
method __init__ (line 56) | def __init__(self, hass, connector, config, code, options):
method device_info (line 101) | def device_info(self):
method get_value (line 125) | def get_value(self):
method get_max_value (line 132) | def get_max_value(self):
method get_max_opc_value (line 138) | def get_max_opc_value(self):
method async_set_native_value (line 152) | async def async_set_native_value(self, value: float) -> None:
method async_update (line 163) | async def async_update(self):
method async_added_to_hass (line 170) | async def async_added_to_hass(self):
method async_update_callback (line 175) | async def async_update_callback(self, isPush: bool = False):
method update_option_listener (line 194) | def update_option_listener(self):
FILE: custom_components/echonetlite/quirks/Nichicon/all/02A5.py
function _sint_4 (line 16) | def _sint_4(edt):
function _02A5F5 (line 21) | def _02A5F5(edt):
function _02A5F6 (line 39) | def _02A5F6(edt):
FILE: custom_components/echonetlite/select.py
function async_setup_entry (line 27) | async def async_setup_entry(hass, config, async_add_entities, discovery_...
class EchonetSelect (line 66) | class EchonetSelect(SelectEntity):
method __init__ (line 87) | def __init__(self, hass, connector, config, code, options):
method device_info (line 138) | def device_info(self):
method async_select_option (line 162) | async def async_select_option(self, option: str):
method async_update (line 172) | async def async_update(self):
method update_attr (line 179) | def update_attr(self):
method async_added_to_hass (line 193) | async def async_added_to_hass(self):
method async_update_callback (line 198) | async def async_update_callback(self, isPush: bool = False):
method update_option_listener (line 215) | def update_option_listener(self):
FILE: custom_components/echonetlite/sensor.py
function async_setup_entry (line 59) | async def async_setup_entry(hass, config, async_add_entities, discovery_...
class EchonetSensor (line 210) | class EchonetSensor(SensorEntity):
method __init__ (line 215) | def __init__(self, connector, config, op_code, attributes, hass=None) ...
method device_info (line 274) | def device_info(self):
method get_attr_native_value (line 296) | def get_attr_native_value(self):
method async_update (line 385) | async def async_update(self):
method async_set_on_timer_time (line 393) | async def async_set_on_timer_time(self, timer_time):
method async_set_value_int_1b (line 403) | async def async_set_value_int_1b(self, value, epc=None):
method async_added_to_hass (line 417) | async def async_added_to_hass(self):
method async_update_callback (line 422) | async def async_update_callback(self, isPush: bool = False):
method update_option_listener (line 448) | def update_option_listener(self):
FILE: custom_components/echonetlite/switch.py
function async_setup_entry (line 26) | async def async_setup_entry(hass, config, async_add_entities, discovery_...
class EchonetSwitch (line 96) | class EchonetSwitch(SwitchEntity):
method __init__ (line 97) | def __init__(self, hass, connector, config, code, options):
method device_info (line 150) | def device_info(self):
method async_turn_on (line 173) | async def async_turn_on(self, **kwargs) -> None:
method async_turn_off (line 201) | async def async_turn_off(self, **kwargs) -> None:
method async_update (line 207) | async def async_update(self):
method async_added_to_hass (line 214) | async def async_added_to_hass(self):
method async_update_callback (line 219) | async def async_update_callback(self, isPush: bool = False):
method update_option_listener (line 236) | def update_option_listener(self):
FILE: custom_components/echonetlite/time.py
function async_setup_entry (line 22) | async def async_setup_entry(hass, config, async_add_entities, discovery_...
class EchonetTime (line 53) | class EchonetTime(TimeEntity):
method __init__ (line 56) | def __init__(self, hass, connector, config, code, options, device_name...
method device_info (line 84) | def device_info(self):
method get_time (line 108) | def get_time(self):
method async_set_value (line 117) | async def async_set_value(self, value: time) -> None:
method async_update (line 129) | async def async_update(self):
method async_added_to_hass (line 136) | async def async_added_to_hass(self):
method async_update_callback (line 141) | async def async_update_callback(self, isPush: bool = False):
method update_option_listener (line 158) | def update_option_listener(self):
Condensed preview — 32 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (315K chars).
[
{
"path": ".github/workflows/black.yml",
"chars": 154,
"preview": "name: Lint\n\non: [push, pull_request]\n\njobs:\n lint:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout"
},
{
"path": ".github/workflows/python-app.yml",
"chars": 1178,
"preview": "# This workflow will install Python dependencies, run tests and lint with a single version of Python\n# For more informat"
},
{
"path": ".gitignore",
"chars": 81,
"preview": "*.pyc\n*.tar.gz\nbuild/*\ndist/*\npychonet.egg-info/*\n.DS_Store\n__pycache__\npychonet\n"
},
{
"path": ".vscode/settings.json",
"chars": 45,
"preview": "{\n \"python.formatting.provider\": \"black\"\n}"
},
{
"path": "LICENSE",
"chars": 1071,
"preview": "MIT License\n\nCopyright (c) 2019 Scott Phillips\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "README.ja.md",
"chars": 5578,
"preview": "# ECHONETLite Platform Custom Component for Home Assistant\n\n[![GitHub Release][releases-shield]][releases]\n[![License][l"
},
{
"path": "README.md",
"chars": 18122,
"preview": "# ECHONETLite Platform Custom Component for Home Assistant\n\n[![GitHub Release][releases-shield]][releases]\n[![License][l"
},
{
"path": "Services.ja.md",
"chars": 1502,
"preview": "# サービスの構成\n\nバージョン 3.5.3 以降、予備的なサポートとして高度なサービス呼び出しが構成されています。 現在、次のデバイスがホームアシスタントサービスをサポートしています。\n\n## ECHONET Lite 対応給湯器 (リン"
},
{
"path": "Services.md",
"chars": 3328,
"preview": "# Configuring Services\n\nPreliminary support for advanced service calls has been configured as of version 3.5.3. At the m"
},
{
"path": "custom_components/echonetlite/__init__.py",
"chars": 28466,
"preview": "\"\"\"The echonetlite integration.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nimport os\nimport t"
},
{
"path": "custom_components/echonetlite/binary_sensor.py",
"chars": 15113,
"preview": "\"\"\"Support for ECHONETLite sensors.\"\"\"\n\nimport logging\nimport voluptuous as vol\n\nfrom homeassistant.const import (\n C"
},
{
"path": "custom_components/echonetlite/climate.py",
"chars": 16926,
"preview": "import logging\nimport math\n\nimport voluptuous as vol\nfrom homeassistant.components.climate import (\n ClimateEntity,\n)"
},
{
"path": "custom_components/echonetlite/config_flow.py",
"chars": 22156,
"preview": "\"\"\"Config flow for echonetlite integration.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport asyncio\nfrom t"
},
{
"path": "custom_components/echonetlite/const.py",
"chars": 53939,
"preview": "\"\"\"Constants for the echonetlite integration.\"\"\"\n\nfrom homeassistant.const import (\n CONF_ICON,\n CONF_SERVICE,\n "
},
{
"path": "custom_components/echonetlite/cover.py",
"chars": 8646,
"preview": "import logging\nimport math\n\nfrom typing import Any\n\nfrom pychonet.lib.epc_functions import (\n DATA_STATE_CLOSE,\n D"
},
{
"path": "custom_components/echonetlite/fan.py",
"chars": 9146,
"preview": "import logging\nfrom pychonet.HomeAirCleaner import FAN_SPEED\nfrom pychonet.lib.const import ENL_STATUS\n\nfrom pychonet.li"
},
{
"path": "custom_components/echonetlite/light.py",
"chars": 15942,
"preview": "import logging\n\nfrom pychonet.GeneralLighting import ENL_STATUS, ENL_BRIGHTNESS, ENL_COLOR_TEMP\nfrom pychonet.CeilingFan"
},
{
"path": "custom_components/echonetlite/manifest.json",
"chars": 440,
"preview": "{\n \"domain\": \"echonetlite\",\n \"name\": \"ECHONET Lite\",\n \"issue_tracker\": \"https://github.com/scottyphillips/echonetlite"
},
{
"path": "custom_components/echonetlite/number.py",
"chars": 7518,
"preview": "import logging\nfrom homeassistant.const import (\n CONF_ICON,\n CONF_NAME,\n CONF_TYPE,\n CONF_MINIMUM,\n CONF"
},
{
"path": "custom_components/echonetlite/quirks/Nichicon/all/02A5.py",
"chars": 1916,
"preview": "from homeassistant.components.sensor.const import (\n CONF_STATE_CLASS,\n SensorDeviceClass,\n SensorStateClass,\n)"
},
{
"path": "custom_components/echonetlite/quirks/Panasonic/all/0135.py",
"chars": 458,
"preview": "from homeassistant.const import CONF_ICON, CONF_NAME\nfrom pychonet.lib.epc_functions import _int\n\nQUIRKS = {\n 0xF1: {"
},
{
"path": "custom_components/echonetlite/select.py",
"chars": 8428,
"preview": "import logging\nfrom homeassistant.const import CONF_ICON, CONF_NAME\nfrom homeassistant.components.select import SelectEn"
},
{
"path": "custom_components/echonetlite/sensor.py",
"chars": 19036,
"preview": "\"\"\"Support for ECHONETLite sensors.\"\"\"\n\nimport logging\nimport voluptuous as vol\n\nfrom homeassistant.const import (\n C"
},
{
"path": "custom_components/echonetlite/services.yaml",
"chars": 888,
"preview": "set_on_timer_time:\n description: 'Set the time of the on-timer of the device.'\n target:\n entity:\n domain: sens"
},
{
"path": "custom_components/echonetlite/strings.json",
"chars": 9574,
"preview": "{\n \"config\": {\n \"step\": {\n \"user\": {\n \"title\": \"Device IP address and name setting ("
},
{
"path": "custom_components/echonetlite/switch.py",
"chars": 9410,
"preview": "import asyncio\nimport logging\nfrom homeassistant.const import CONF_ICON, CONF_SERVICE_DATA, CONF_NAME\nfrom homeassistant"
},
{
"path": "custom_components/echonetlite/time.py",
"chars": 6193,
"preview": "import logging\nimport datetime\nfrom datetime import time\nfrom homeassistant.const import CONF_ICON, CONF_NAME\nfrom homea"
},
{
"path": "custom_components/echonetlite/translations/en.json",
"chars": 9237,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"Device is already configured\"\n },\n "
},
{
"path": "custom_components/echonetlite/translations/ja.json",
"chars": 8084,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"デバイスは構成済みです\"\n },\n \"error\": {\n "
},
{
"path": "custom_components/echonetlite/translations/pt.json",
"chars": 6971,
"preview": "{\n \"config\": {\n \"abort\": {\n \"already_configured\": \"Dispositivo já configurado\"\n },\n \"error\": {\n \"can"
},
{
"path": "hacs.json",
"chars": 221,
"preview": "{\n \"name\": \"ECHONETLite Platform\",\n \"render_readme\": true,\n \"domains\": [\"climate\", \"sensor\", \"select\", \"light\", \"fan\""
},
{
"path": "info.md",
"chars": 4237,
"preview": "[![GitHub Release][releases-shield]][releases]\n[![License][license-shield]](LICENSE)\n[![hacs][hacsbadge]][hacs]\n![Projec"
}
]
About this extraction
This page contains the full source code of the scottyphillips/echonetlite_homeassistant GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 32 files (287.1 KB), approximately 67.8k tokens, and a symbol index with 168 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.