master 0995f2817b74 cached
32 files
287.1 KB
67.8k tokens
168 symbols
1 requests
Download .txt
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\
   [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=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]
   
Download .txt
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
Download .txt
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.

Copied to clipboard!