esp32io-mqtt 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Noritama-Lab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,233 @@
1
+ Metadata-Version: 2.4
2
+ Name: esp32io-mqtt
3
+ Version: 0.1.0
4
+ Summary: A lightweight Python MQTT client for controlling ESP32IO-MQTT devices (Home Assistant compatible).
5
+ Author: Noritama-Lab
6
+ License: MIT
7
+ Project-URL: Repository, https://github.com/Noritama-Lab/esp32io-mqtt-api
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: System :: Hardware
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: paho-mqtt<2.0,>=1.6
23
+ Dynamic: license-file
24
+
25
+ # ESP32IO-MQTT Python MQTT Client
26
+
27
+ ESP32IO-MQTT is a lightweight Python **MQTT** client for controlling [ESP32IO-MQTT](https://github.com/Noritama-Lab/esp32io-mqtt-firmware) devices.
28
+
29
+ 日本語 / English
30
+
31
+ ---
32
+
33
+ ## 日本語
34
+
35
+ ESP32IO-MQTT Pythonクライアントは、**MQTTブローカー** 経由で ESP32IO-MQTT ファームウェアを制御するための軽量Pythonクライアントです。
36
+
37
+ ESP32IO-MQTT ファームウェアはデバイス制御を **MQTT** 経由で行います。本クライアントもMQTT専用です。
38
+
39
+ ---
40
+
41
+ ## 特徴
42
+
43
+ - MQTTブローカー経由での接続のみをサポート(HTTP/シリアルは非対応)
44
+ - 汎用JSONコマンドチャネル (`<base>/cmd` → `<base>/cmd/result`) を使った同期API (`read_di()`, `set_do()`, `get_io_state()` 等)
45
+ - 個別エンティティのトピック (Home Assistant MQTT Discovery互換) にも対応
46
+ - `get_cached_state()` で直近受信した状態のスナップショットを取得
47
+ - `on_state_change()` で状態変化のコールバックを登録
48
+ - `publish_do()` / `publish_pwm()` / `publish_rgb()` / `publish_led_off()` / `publish_led_mode()` / `publish_oled()` で応答待ちなしに直接publish
49
+ - I2Cスキャン、リード/ライト、BME280/MPU6050/VL53L1Xなどのセンサー一括取得、OLED表示をサポート
50
+ - PWM周波数・分解能の取得/更新に対応
51
+ - 通信エラー・タイムアウト・プロトコル不整合・デバイスエラーなど統一例外を定義
52
+ - 任意JSONを送れる低レベルAPI `command()` を提供
53
+
54
+ ---
55
+
56
+ ## 動作環境
57
+
58
+ - Python 3.8 以上
59
+ - `paho-mqtt>=1.6,<2.0`
60
+ - MQTTブローカー (Mosquitto等)
61
+ - ESP32IO-MQTT ファームウェア(MQTT対応)
62
+
63
+ ---
64
+
65
+ ## インストール
66
+
67
+ ```bash
68
+ pip install -r requirements.txt
69
+ # または
70
+ pip install -e .
71
+ ```
72
+
73
+ ---
74
+
75
+ ## クイックスタート
76
+
77
+ ```python
78
+ from esp32io_mqtt import ESP32S3IOMqtt
79
+
80
+ # DEVICE_ID は設定ポータル (http://<device_ip>/) のページタイトルや
81
+ # mDNSホスト名 (ESP32IO_MQTT_XXXXXX) で確認できます。
82
+ with ESP32S3IOMqtt("ESP32IO_MQTT_XXXXXX", "192.168.1.5", debug=False) as esp:
83
+ print("ping =", esp.ping())
84
+ print("di0 =", esp.read_di(0))
85
+ print("adc0 =", esp.read_adc(0))
86
+ print("pwm config =", esp.get_pwm_config())
87
+
88
+ esp.set_do(0, 1)
89
+ esp.set_pwm(0, 128)
90
+
91
+ print(esp.get_io_state())
92
+ ```
93
+
94
+ サンプル実行:
95
+
96
+ ```bash
97
+ py -m examples.mqtt_example
98
+ ```
99
+
100
+ ---
101
+
102
+ ## 通信方式
103
+
104
+ ### 1. 汎用JSONコマンドチャネル (同期API)
105
+
106
+ `command()` および `read_di()` / `set_do()` などの高レベルメソッドは、`<base>/cmd` にJSONをpublishし `<base>/cmd/result` の応答を待つ同期方式です。戻り値やエラー(`ESP32IODeviceError`)で結果を確認できます。
107
+
108
+ ### 2. 個別エンティティのトピック (Home Assistant MQTT Discovery互換)
109
+
110
+ 接続時に `di/+/state` `do/+/state` `pwm/+/state` `rgb/state` `led_mode/state` `oled/state` `bme280/#` `mpu6050/#` `vl53l1x/#` `diag/#` `status` を自動的にsubscribeし、受信するたびに内部キャッシュへ保存します。
111
+
112
+ - `get_cached_state()` — 直近に受信した値のスナップショット (dict) を取得
113
+ - `on_state_change(callback)` — 状態トピックを受信するたびに `callback(topic_suffix, payload)` を呼び出す
114
+ - `is_online` — LWTトピック (`<base>/status`) が `online` かどうか
115
+ - `publish_do(pin_id, value)` / `publish_pwm(pin_id, duty)` / `publish_rgb(r, g, b, brightness)` / `publish_led_off()` / `publish_led_mode(mode)` / `publish_oled(text)` — 応答を待たずに直接publish(Home Assistant側からの操作と同じ経路)
116
+
117
+ ---
118
+
119
+ ## API 一覧(主要メソッド)
120
+
121
+ - `ESP32S3IOMqtt(device_id, host, port=1883, user=None, password=None, topic_root="esp32io", keepalive=30, cmd_timeout=5.0, connect_timeout=10.0, debug=False)`
122
+ - `ping()`
123
+ - `read_di(pin_id)` / `set_do(pin_id, value)`
124
+ - `read_adc(pin_id)`
125
+ - `set_pwm(pin_id, duty)`
126
+ - `get_pwm_config()` / `set_pwm_config(freq, res)`
127
+ - `set_led_mode(mode)` / `set_rgb(r, g, b, brightness)` / `led_off()` / `get_led_state()`
128
+ - `i2c_scan()` / `i2c_read(addr, length)` / `i2c_write(addr, data)`
129
+ - `get_sensors()`
130
+ - `set_oled(text, x, y, size, clear)`
131
+ - `get_io_state()` / `get_status()`
132
+ - `command(cmd, **kwargs)`
133
+ - `help()`
134
+ - `get_cached_state()` / `on_state_change(callback)` / `is_online`
135
+ - `publish_do()` / `publish_pwm()` / `publish_rgb()` / `publish_led_off()` / `publish_led_mode()` / `publish_oled()`
136
+ - `close()`
137
+
138
+ ---
139
+
140
+ ## 例外
141
+
142
+ - `ESP32IOError`(基底クラス)
143
+ - `ESP32IOMqttError` — ブローカー接続・publish失敗
144
+ - `ESP32IOTimeoutError` — `cmd/result` 応答待ちのタイムアウト(`ESP32IOMqttError`のサブクラス)
145
+ - `ESP32IOProtocolError` — デバイスから不正な応答
146
+ - `ESP32IODeviceError` — デバイスが `status=error` を返した場合
147
+
148
+ ---
149
+
150
+ ## プロジェクト構成
151
+
152
+ ```text
153
+ .
154
+ ├── esp32io_mqtt/
155
+ │ ├── __init__.py
156
+ │ ├── client.py
157
+ │ ├── exceptions.py
158
+ │ └── protocol.py
159
+ ├── examples/
160
+ │ ├── mqtt_example.py
161
+ │ └── i2c_example.py
162
+ ├── pyproject.toml
163
+ ├── requirements.txt
164
+ ├── LICENSE
165
+ └── README.md
166
+ ```
167
+
168
+ ---
169
+
170
+ ## ファームウェア
171
+
172
+ 対応ファームウェア: [ESP32IO-MQTT](https://github.com/Noritama-Lab/esp32io-mqtt-firmware)(MQTT + Home Assistant MQTT Discovery対応)。
173
+ トピック構成・汎用JSONコマンド一覧は同リポジトリの `COMMAND_REFERENCE.md` を参照してください。
174
+
175
+ ---
176
+
177
+ ## ライセンス
178
+
179
+ MIT License
180
+ Copyright (c) 2026 Noritama-Lab
181
+
182
+ ---
183
+
184
+ ## English
185
+
186
+ ESP32IO-MQTT Python client is a lightweight **MQTT** client for controlling [ESP32IO-MQTT](https://github.com/Noritama-Lab/esp32io-mqtt-firmware) firmware.
187
+
188
+ ESP32IO-MQTT firmware controls devices over **MQTT**. This client is MQTT-only.
189
+
190
+ ### Features
191
+
192
+ - MQTT broker connection only (no HTTP/Serial support)
193
+ - Synchronous API over the generic JSON command channel (`<base>/cmd` → `<base>/cmd/result`) (`read_di()`, `set_do()`, `get_io_state()`, etc.)
194
+ - Support for individual entity topics (Home Assistant MQTT Discovery compatible)
195
+ - `get_cached_state()` for a snapshot of the latest received state
196
+ - `on_state_change()` to register a callback for state changes
197
+ - `publish_do()` / `publish_pwm()` / `publish_rgb()` / `publish_led_off()` / `publish_led_mode()` / `publish_oled()` to publish directly without waiting for a response
198
+ - I2C scanning, raw read/write, sensor batch reads (BME280/MPU6050/VL53L1X), and OLED control
199
+ - PWM frequency/resolution read & update
200
+ - Unified exceptions for MQTT, protocol, timeout, and device errors
201
+ - Low-level `command()` API for custom JSON commands
202
+
203
+ ### Requirements
204
+
205
+ - Python 3.8+
206
+ - `paho-mqtt>=1.6,<2.0`
207
+ - An MQTT broker (e.g. Mosquitto)
208
+ - ESP32IO-MQTT firmware (MQTT-based)
209
+
210
+ ### Installation
211
+
212
+ ```bash
213
+ pip install -r requirements.txt
214
+ # or
215
+ pip install -e .
216
+ ```
217
+
218
+ ### Quick Start
219
+
220
+ ```python
221
+ from esp32io_mqtt import ESP32S3IOMqtt
222
+
223
+ with ESP32S3IOMqtt("ESP32IO_MQTT_XXXXXX", "192.168.1.5", debug=False) as esp:
224
+ print("ping =", esp.ping())
225
+ print("di0 =", esp.read_di(0))
226
+ esp.set_do(0, 1)
227
+ print(esp.get_io_state())
228
+ ```
229
+
230
+ ### License
231
+
232
+ MIT License
233
+ Copyright (c) 2026 Noritama-Lab
@@ -0,0 +1,209 @@
1
+ # ESP32IO-MQTT Python MQTT Client
2
+
3
+ ESP32IO-MQTT is a lightweight Python **MQTT** client for controlling [ESP32IO-MQTT](https://github.com/Noritama-Lab/esp32io-mqtt-firmware) devices.
4
+
5
+ 日本語 / English
6
+
7
+ ---
8
+
9
+ ## 日本語
10
+
11
+ ESP32IO-MQTT Pythonクライアントは、**MQTTブローカー** 経由で ESP32IO-MQTT ファームウェアを制御するための軽量Pythonクライアントです。
12
+
13
+ ESP32IO-MQTT ファームウェアはデバイス制御を **MQTT** 経由で行います。本クライアントもMQTT専用です。
14
+
15
+ ---
16
+
17
+ ## 特徴
18
+
19
+ - MQTTブローカー経由での接続のみをサポート(HTTP/シリアルは非対応)
20
+ - 汎用JSONコマンドチャネル (`<base>/cmd` → `<base>/cmd/result`) を使った同期API (`read_di()`, `set_do()`, `get_io_state()` 等)
21
+ - 個別エンティティのトピック (Home Assistant MQTT Discovery互換) にも対応
22
+ - `get_cached_state()` で直近受信した状態のスナップショットを取得
23
+ - `on_state_change()` で状態変化のコールバックを登録
24
+ - `publish_do()` / `publish_pwm()` / `publish_rgb()` / `publish_led_off()` / `publish_led_mode()` / `publish_oled()` で応答待ちなしに直接publish
25
+ - I2Cスキャン、リード/ライト、BME280/MPU6050/VL53L1Xなどのセンサー一括取得、OLED表示をサポート
26
+ - PWM周波数・分解能の取得/更新に対応
27
+ - 通信エラー・タイムアウト・プロトコル不整合・デバイスエラーなど統一例外を定義
28
+ - 任意JSONを送れる低レベルAPI `command()` を提供
29
+
30
+ ---
31
+
32
+ ## 動作環境
33
+
34
+ - Python 3.8 以上
35
+ - `paho-mqtt>=1.6,<2.0`
36
+ - MQTTブローカー (Mosquitto等)
37
+ - ESP32IO-MQTT ファームウェア(MQTT対応)
38
+
39
+ ---
40
+
41
+ ## インストール
42
+
43
+ ```bash
44
+ pip install -r requirements.txt
45
+ # または
46
+ pip install -e .
47
+ ```
48
+
49
+ ---
50
+
51
+ ## クイックスタート
52
+
53
+ ```python
54
+ from esp32io_mqtt import ESP32S3IOMqtt
55
+
56
+ # DEVICE_ID は設定ポータル (http://<device_ip>/) のページタイトルや
57
+ # mDNSホスト名 (ESP32IO_MQTT_XXXXXX) で確認できます。
58
+ with ESP32S3IOMqtt("ESP32IO_MQTT_XXXXXX", "192.168.1.5", debug=False) as esp:
59
+ print("ping =", esp.ping())
60
+ print("di0 =", esp.read_di(0))
61
+ print("adc0 =", esp.read_adc(0))
62
+ print("pwm config =", esp.get_pwm_config())
63
+
64
+ esp.set_do(0, 1)
65
+ esp.set_pwm(0, 128)
66
+
67
+ print(esp.get_io_state())
68
+ ```
69
+
70
+ サンプル実行:
71
+
72
+ ```bash
73
+ py -m examples.mqtt_example
74
+ ```
75
+
76
+ ---
77
+
78
+ ## 通信方式
79
+
80
+ ### 1. 汎用JSONコマンドチャネル (同期API)
81
+
82
+ `command()` および `read_di()` / `set_do()` などの高レベルメソッドは、`<base>/cmd` にJSONをpublishし `<base>/cmd/result` の応答を待つ同期方式です。戻り値やエラー(`ESP32IODeviceError`)で結果を確認できます。
83
+
84
+ ### 2. 個別エンティティのトピック (Home Assistant MQTT Discovery互換)
85
+
86
+ 接続時に `di/+/state` `do/+/state` `pwm/+/state` `rgb/state` `led_mode/state` `oled/state` `bme280/#` `mpu6050/#` `vl53l1x/#` `diag/#` `status` を自動的にsubscribeし、受信するたびに内部キャッシュへ保存します。
87
+
88
+ - `get_cached_state()` — 直近に受信した値のスナップショット (dict) を取得
89
+ - `on_state_change(callback)` — 状態トピックを受信するたびに `callback(topic_suffix, payload)` を呼び出す
90
+ - `is_online` — LWTトピック (`<base>/status`) が `online` かどうか
91
+ - `publish_do(pin_id, value)` / `publish_pwm(pin_id, duty)` / `publish_rgb(r, g, b, brightness)` / `publish_led_off()` / `publish_led_mode(mode)` / `publish_oled(text)` — 応答を待たずに直接publish(Home Assistant側からの操作と同じ経路)
92
+
93
+ ---
94
+
95
+ ## API 一覧(主要メソッド)
96
+
97
+ - `ESP32S3IOMqtt(device_id, host, port=1883, user=None, password=None, topic_root="esp32io", keepalive=30, cmd_timeout=5.0, connect_timeout=10.0, debug=False)`
98
+ - `ping()`
99
+ - `read_di(pin_id)` / `set_do(pin_id, value)`
100
+ - `read_adc(pin_id)`
101
+ - `set_pwm(pin_id, duty)`
102
+ - `get_pwm_config()` / `set_pwm_config(freq, res)`
103
+ - `set_led_mode(mode)` / `set_rgb(r, g, b, brightness)` / `led_off()` / `get_led_state()`
104
+ - `i2c_scan()` / `i2c_read(addr, length)` / `i2c_write(addr, data)`
105
+ - `get_sensors()`
106
+ - `set_oled(text, x, y, size, clear)`
107
+ - `get_io_state()` / `get_status()`
108
+ - `command(cmd, **kwargs)`
109
+ - `help()`
110
+ - `get_cached_state()` / `on_state_change(callback)` / `is_online`
111
+ - `publish_do()` / `publish_pwm()` / `publish_rgb()` / `publish_led_off()` / `publish_led_mode()` / `publish_oled()`
112
+ - `close()`
113
+
114
+ ---
115
+
116
+ ## 例外
117
+
118
+ - `ESP32IOError`(基底クラス)
119
+ - `ESP32IOMqttError` — ブローカー接続・publish失敗
120
+ - `ESP32IOTimeoutError` — `cmd/result` 応答待ちのタイムアウト(`ESP32IOMqttError`のサブクラス)
121
+ - `ESP32IOProtocolError` — デバイスから不正な応答
122
+ - `ESP32IODeviceError` — デバイスが `status=error` を返した場合
123
+
124
+ ---
125
+
126
+ ## プロジェクト構成
127
+
128
+ ```text
129
+ .
130
+ ├── esp32io_mqtt/
131
+ │ ├── __init__.py
132
+ │ ├── client.py
133
+ │ ├── exceptions.py
134
+ │ └── protocol.py
135
+ ├── examples/
136
+ │ ├── mqtt_example.py
137
+ │ └── i2c_example.py
138
+ ├── pyproject.toml
139
+ ├── requirements.txt
140
+ ├── LICENSE
141
+ └── README.md
142
+ ```
143
+
144
+ ---
145
+
146
+ ## ファームウェア
147
+
148
+ 対応ファームウェア: [ESP32IO-MQTT](https://github.com/Noritama-Lab/esp32io-mqtt-firmware)(MQTT + Home Assistant MQTT Discovery対応)。
149
+ トピック構成・汎用JSONコマンド一覧は同リポジトリの `COMMAND_REFERENCE.md` を参照してください。
150
+
151
+ ---
152
+
153
+ ## ライセンス
154
+
155
+ MIT License
156
+ Copyright (c) 2026 Noritama-Lab
157
+
158
+ ---
159
+
160
+ ## English
161
+
162
+ ESP32IO-MQTT Python client is a lightweight **MQTT** client for controlling [ESP32IO-MQTT](https://github.com/Noritama-Lab/esp32io-mqtt-firmware) firmware.
163
+
164
+ ESP32IO-MQTT firmware controls devices over **MQTT**. This client is MQTT-only.
165
+
166
+ ### Features
167
+
168
+ - MQTT broker connection only (no HTTP/Serial support)
169
+ - Synchronous API over the generic JSON command channel (`<base>/cmd` → `<base>/cmd/result`) (`read_di()`, `set_do()`, `get_io_state()`, etc.)
170
+ - Support for individual entity topics (Home Assistant MQTT Discovery compatible)
171
+ - `get_cached_state()` for a snapshot of the latest received state
172
+ - `on_state_change()` to register a callback for state changes
173
+ - `publish_do()` / `publish_pwm()` / `publish_rgb()` / `publish_led_off()` / `publish_led_mode()` / `publish_oled()` to publish directly without waiting for a response
174
+ - I2C scanning, raw read/write, sensor batch reads (BME280/MPU6050/VL53L1X), and OLED control
175
+ - PWM frequency/resolution read & update
176
+ - Unified exceptions for MQTT, protocol, timeout, and device errors
177
+ - Low-level `command()` API for custom JSON commands
178
+
179
+ ### Requirements
180
+
181
+ - Python 3.8+
182
+ - `paho-mqtt>=1.6,<2.0`
183
+ - An MQTT broker (e.g. Mosquitto)
184
+ - ESP32IO-MQTT firmware (MQTT-based)
185
+
186
+ ### Installation
187
+
188
+ ```bash
189
+ pip install -r requirements.txt
190
+ # or
191
+ pip install -e .
192
+ ```
193
+
194
+ ### Quick Start
195
+
196
+ ```python
197
+ from esp32io_mqtt import ESP32S3IOMqtt
198
+
199
+ with ESP32S3IOMqtt("ESP32IO_MQTT_XXXXXX", "192.168.1.5", debug=False) as esp:
200
+ print("ping =", esp.ping())
201
+ print("di0 =", esp.read_di(0))
202
+ esp.set_do(0, 1)
203
+ print(esp.get_io_state())
204
+ ```
205
+
206
+ ### License
207
+
208
+ MIT License
209
+ Copyright (c) 2026 Noritama-Lab
@@ -0,0 +1,17 @@
1
+ from .client import ESP32S3IOMqtt
2
+ from .exceptions import (
3
+ ESP32IOError,
4
+ ESP32IODeviceError,
5
+ ESP32IOMqttError,
6
+ ESP32IOTimeoutError,
7
+ ESP32IOProtocolError,
8
+ )
9
+
10
+ __all__ = [
11
+ "ESP32S3IOMqtt",
12
+ "ESP32IOError",
13
+ "ESP32IODeviceError",
14
+ "ESP32IOMqttError",
15
+ "ESP32IOTimeoutError",
16
+ "ESP32IOProtocolError",
17
+ ]
@@ -0,0 +1,345 @@
1
+ import json
2
+ import uuid
3
+ import logging
4
+ import threading
5
+ from typing import Optional, Dict, Any, List, Literal, Callable
6
+
7
+ import paho.mqtt.client as mqtt
8
+
9
+ from .exceptions import (
10
+ ESP32IODeviceError,
11
+ ESP32IOMqttError,
12
+ ESP32IOTimeoutError,
13
+ ESP32IOProtocolError,
14
+ )
15
+ from . import protocol
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class ESP32S3IOMqtt:
21
+ """MQTTブローカー経由でESP32IO-MQTTデバイスを操作するクライアント。
22
+
23
+ デバイス制御には2通りの経路がある:
24
+ - 汎用JSONコマンドチャネル (``command()`` および各 ``read_*``/``set_*`` メソッド):
25
+ `<base>/cmd` にpublishし `<base>/cmd/result` の応答を同期的に待つ。旧HTTP/Serial版と同じ使用感。
26
+ - 個別エンティティのトピック (Home Assistant MQTT Discovery互換):
27
+ `get_cached_state()` で直近の受信値を参照でき、``publish_*`` 系メソッドで
28
+ 応答待ちなしに状態変更を発行できる。
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ device_id: str,
34
+ host: str,
35
+ port: int = 1883,
36
+ user: Optional[str] = None,
37
+ password: Optional[str] = None,
38
+ topic_root: str = protocol.DEFAULT_TOPIC_ROOT,
39
+ keepalive: int = 30,
40
+ cmd_timeout: float = 5.0,
41
+ connect_timeout: float = 10.0,
42
+ debug: bool = False,
43
+ ):
44
+ if debug:
45
+ logging.basicConfig(level=logging.DEBUG)
46
+
47
+ self.device_id = device_id
48
+ self.base = f"{topic_root}/{device_id}"
49
+ self.cmd_timeout = cmd_timeout
50
+
51
+ self._state: Dict[str, str] = {}
52
+ self._state_lock = threading.Lock()
53
+ self._listeners: List[Callable[[str, str], None]] = []
54
+
55
+ self._connected_event = threading.Event()
56
+ self._cmd_lock = threading.Lock() # 汎用コマンドチャネルは一度に1件のみ処理
57
+ self._cmd_response_event = threading.Event()
58
+ self._cmd_response: Optional[Dict[str, Any]] = None
59
+
60
+ self._client = mqtt.Client(client_id=f"esp32io-mqtt-py-{uuid.uuid4().hex[:8]}")
61
+ if user:
62
+ self._client.username_pw_set(user, password or "")
63
+ self._client.on_connect = self._on_connect
64
+ self._client.on_disconnect = self._on_disconnect
65
+ self._client.on_message = self._on_message
66
+
67
+ try:
68
+ self._client.connect(host, port, keepalive=keepalive)
69
+ except OSError as e:
70
+ raise ESP32IOMqttError(f"Failed to connect to broker {host}:{port}: {e}")
71
+
72
+ self._client.loop_start()
73
+ if not self._connected_event.wait(connect_timeout):
74
+ self._client.loop_stop()
75
+ raise ESP32IOMqttError(f"Timed out connecting to broker {host}:{port}")
76
+
77
+ def __enter__(self):
78
+ return self
79
+
80
+ def __exit__(self, _exc_type, _exc_val, _exc_tb):
81
+ self.close()
82
+
83
+ def _log(self, *args: Any) -> None:
84
+ logger.debug(" ".join(map(str, args)))
85
+
86
+ # ------------------------------------------------------------------
87
+ # 接続 / 購読 / 状態キャッシュ
88
+ # ------------------------------------------------------------------
89
+ def _on_connect(self, client, _userdata, _flags, rc):
90
+ if rc != 0:
91
+ logger.error("MQTT connect failed rc=%s", rc)
92
+ return
93
+ client.subscribe(f"{self.base}/cmd/result")
94
+ for pattern in protocol.ENTITY_SUBSCRIBE_TOPICS:
95
+ client.subscribe(pattern.format(base=self.base))
96
+ self._connected_event.set()
97
+
98
+ def _on_disconnect(self, _client, _userdata, _rc):
99
+ self._connected_event.clear()
100
+
101
+ def _on_message(self, _client, _userdata, msg):
102
+ payload = msg.payload.decode("utf-8", errors="replace")
103
+ self._log(f"RECV: {msg.topic} {payload}")
104
+
105
+ if msg.topic == f"{self.base}/cmd/result":
106
+ try:
107
+ self._cmd_response = json.loads(payload)
108
+ except json.JSONDecodeError:
109
+ logger.warning("Invalid JSON on cmd/result: %s", payload)
110
+ return
111
+ self._cmd_response_event.set()
112
+ return
113
+
114
+ suffix = msg.topic[len(self.base) + 1:]
115
+ with self._state_lock:
116
+ self._state[suffix] = payload
117
+ for listener in list(self._listeners):
118
+ try:
119
+ listener(suffix, payload)
120
+ except Exception:
121
+ logger.exception("State listener raised an exception")
122
+
123
+ def on_state_change(self, callback: Callable[[str, str], None]) -> None:
124
+ """個別エンティティのトピック受信ごとに ``callback(topic_suffix, payload)`` を呼び出す。"""
125
+ self._listeners.append(callback)
126
+
127
+ def get_cached_state(self) -> Dict[str, str]:
128
+ """個別エンティティのトピックから直近に受信した値のスナップショットを返す。"""
129
+ with self._state_lock:
130
+ return dict(self._state)
131
+
132
+ @property
133
+ def is_online(self) -> bool:
134
+ """デバイスのLWTトピック (`<base>/status`) が ``online`` かどうか。"""
135
+ return self._state.get("status") == "online"
136
+
137
+ # ------------------------------------------------------------------
138
+ # 汎用JSONコマンドチャネル (`<base>/cmd` -> `<base>/cmd/result`)
139
+ # ------------------------------------------------------------------
140
+ def command(self, cmd: str, **kwargs: Any) -> Dict[str, Any]:
141
+ """任意のコマンドを送信し、JSON応答を返す低レベルAPI"""
142
+ payload = {"cmd": cmd}
143
+ payload.update(kwargs)
144
+ return self._execute(payload)
145
+
146
+ def _execute(self, payload: Dict[str, Any]) -> Dict[str, Any]:
147
+ cmd_name = payload.get("cmd", "unknown")
148
+ with self._cmd_lock:
149
+ self._cmd_response = None
150
+ self._cmd_response_event.clear()
151
+
152
+ line = json.dumps(payload, separators=(",", ":"))
153
+ self._log(f"SEND: {line}")
154
+ info = self._client.publish(f"{self.base}/cmd", line, qos=0)
155
+ if info.rc != mqtt.MQTT_ERR_SUCCESS:
156
+ raise ESP32IOMqttError(f"Publish failed for '{cmd_name}': rc={info.rc}")
157
+
158
+ if not self._cmd_response_event.wait(self.cmd_timeout):
159
+ raise ESP32IOTimeoutError(f"Timeout waiting for response to '{cmd_name}'")
160
+
161
+ response = self._cmd_response
162
+ assert response is not None
163
+ return self._process_response(response, cmd_name)
164
+
165
+ def _process_response(self, data: Dict[str, Any], cmd: str) -> Dict[str, Any]:
166
+ if data.get("status") == "error":
167
+ code = data.get("code", "UNKNOWN")
168
+ detail = data.get("detail", "")
169
+ raise ESP32IODeviceError(f"Device error on '{cmd}': {code} ({detail})")
170
+ return data
171
+
172
+ def ping(self) -> bool:
173
+ """疎通確認"""
174
+ res = self.command(protocol.CMD_PING)
175
+ return res.get("message") == "pong"
176
+
177
+ def get_status(self) -> Dict[str, Any]:
178
+ """デバイスの稼働状況(起動時間、ヒープ、IP等)を取得"""
179
+ return self.command(protocol.CMD_GET_STATUS)
180
+
181
+ def get_io_state(self) -> Dict[str, Any]:
182
+ """DI, DO, ADC, PWM の全状態を一括取得"""
183
+ res = self.command(protocol.CMD_GET_IO_STATE)
184
+ return {
185
+ "dio_in": list(res.get("dio_in", [])),
186
+ "dio_out": list(res.get("dio_out", [])),
187
+ "adc": list(res.get("adc", [])),
188
+ "pwm": list(res.get("pwm", [])),
189
+ }
190
+
191
+ def read_di(self, pin_id: int) -> int:
192
+ """デジタル入力の読み取り"""
193
+ if not isinstance(pin_id, int):
194
+ raise ValueError("pin_id must be an integer")
195
+ res = self.command(protocol.CMD_READ_DI, pin_id=pin_id)
196
+ val = res.get("value")
197
+ if not isinstance(val, int):
198
+ raise ESP32IOProtocolError(f"Invalid read_di response: {res}")
199
+ return val
200
+
201
+ def set_do(self, pin_id: int, value: int) -> bool:
202
+ """デジタル出力の設定 (0 or 1)"""
203
+ if value not in (0, 1):
204
+ raise ValueError("value must be 0 or 1")
205
+ res = self.command(protocol.CMD_SET_DO, pin_id=pin_id, value=value)
206
+ return res.get("status") == "ok"
207
+
208
+ def read_adc(self, pin_id: int) -> int:
209
+ """アナログ入力の読み取り"""
210
+ if not isinstance(pin_id, int):
211
+ raise ValueError("pin_id must be an integer")
212
+ res = self.command(protocol.CMD_READ_ADC, pin_id=pin_id)
213
+ val = res.get("value")
214
+ if not isinstance(val, int):
215
+ raise ESP32IOProtocolError(f"Invalid read_adc response: {res}")
216
+ return val
217
+
218
+ def set_pwm(self, pin_id: int, duty: int) -> bool:
219
+ """PWM出力の設定"""
220
+ if duty < 0:
221
+ raise ValueError("duty must be 0 or greater")
222
+ res = self.command(protocol.CMD_SET_PWM, pin_id=pin_id, duty=duty)
223
+ return res.get("status") == "ok"
224
+
225
+ def get_pwm_config(self) -> Dict[str, int]:
226
+ """現在のPWM周波数、分解能、および最大デューティを取得"""
227
+ res = self.command(protocol.CMD_GET_PWM_CONFIG)
228
+ if not all(isinstance(res.get(k), int) for k in ("freq", "res", "max_duty")):
229
+ raise ESP32IOProtocolError(f"Invalid get_pwm_config response: {res}")
230
+ return {"freq": res["freq"], "res": res["res"], "max_duty": res["max_duty"]}
231
+
232
+ def set_pwm_config(self, freq: int, res_bit: int) -> bool:
233
+ """PWMの周波数と分解能を動的に変更"""
234
+ res = self.command(protocol.CMD_SET_PWM_CONFIG, freq=freq, res=res_bit)
235
+ return res.get("status") == "ok"
236
+
237
+ def set_rgb(self, r: int, g: int, b: int, brightness: Optional[int] = None) -> bool:
238
+ """内蔵RGB LEDの制御"""
239
+ if any(not (0 <= c <= 255) for c in (r, g, b)):
240
+ raise ValueError("RGB values must be between 0 and 255")
241
+ kwargs = {"r": r, "g": g, "b": b}
242
+ if brightness is not None:
243
+ kwargs["brightness"] = brightness
244
+ res = self.command(protocol.CMD_SET_RGB, **kwargs)
245
+ return res.get("status") == "ok"
246
+
247
+ def led_off(self) -> bool:
248
+ """RGB LEDの消灯"""
249
+ res = self.command(protocol.CMD_LED_OFF)
250
+ return res.get("status") == "ok"
251
+
252
+ def set_led_mode(self, mode: Literal["status", "manual"]) -> bool:
253
+ """LEDの動作モードを設定 ('status' または 'manual')"""
254
+ res = self.command(protocol.CMD_SET_LED_MODE, mode=mode)
255
+ return res.get("status") == "ok"
256
+
257
+ def get_led_state(self) -> Dict[str, Any]:
258
+ """現在のLED設定値を取得"""
259
+ return self.command(protocol.CMD_GET_LED_STATE)
260
+
261
+ def help(self) -> List[str]:
262
+ """デバイスがサポートするコマンド一覧を取得"""
263
+ res = self.command(protocol.CMD_HELP)
264
+ return res.get("commands", [])
265
+
266
+ def i2c_scan(self) -> List[int]:
267
+ """I2C バスをスキャンし、応答のあったデバイスのアドレス一覧を返す"""
268
+ res = self.command(protocol.CMD_I2C_SCAN)
269
+ return res.get("devices", [])
270
+
271
+ def i2c_read(self, addr: int, length: int) -> List[int]:
272
+ """I2C デバイスからデータを読み取る"""
273
+ if not isinstance(addr, int) or not isinstance(length, int):
274
+ raise ValueError("addr and length must be integers")
275
+ res = self.command(protocol.CMD_I2C_READ, addr=addr, len=length)
276
+ data = res.get("data")
277
+ if not isinstance(data, list):
278
+ raise ESP32IOProtocolError(f"Invalid i2c_read response: {res}")
279
+ return data
280
+
281
+ def i2c_write(self, addr: int, data: List[int]) -> bool:
282
+ """I2C デバイスにデータを書き込む"""
283
+ if not isinstance(addr, int) or not isinstance(data, list):
284
+ raise ValueError("addr must be int and data must be a list of integers")
285
+ res = self.command(protocol.CMD_I2C_WRITE, addr=addr, data=data)
286
+ return res.get("status") == "ok"
287
+
288
+ def get_sensors(self) -> Dict[str, Any]:
289
+ """BME280, MPU6050, VL53L1X などの接続済みセンサー値を一括取得"""
290
+ return self.command(protocol.CMD_GET_SENSORS)
291
+
292
+ def set_oled(self, text: str, x: int = 0, y: int = 0, size: int = 1, clear: bool = True) -> bool:
293
+ """SSD1306 OLED ディスプレイにテキストを表示"""
294
+ res = self.command(protocol.CMD_SET_OLED, text=text, x=x, y=y, size=size, clear=clear)
295
+ return res.get("status") == "ok"
296
+
297
+ # ------------------------------------------------------------------
298
+ # 個別エンティティの直接publish (応答待ちなし。Home Assistant側の操作と同等)
299
+ # ------------------------------------------------------------------
300
+ def publish_do(self, pin_id: int, value: int) -> None:
301
+ """`<base>/do/<pin_id>/set` へ直接publishする(応答は待たない)"""
302
+ if value not in (0, 1):
303
+ raise ValueError("value must be 0 or 1")
304
+ self._client.publish(f"{self.base}/do/{pin_id}/set", "ON" if value else "OFF")
305
+
306
+ def publish_pwm(self, pin_id: int, duty: int) -> None:
307
+ """`<base>/pwm/<pin_id>/set` へ直接publishする(応答は待たない)"""
308
+ if duty < 0:
309
+ raise ValueError("duty must be 0 or greater")
310
+ self._client.publish(f"{self.base}/pwm/{pin_id}/set", str(duty))
311
+
312
+ def publish_rgb(self, r: int, g: int, b: int, brightness: Optional[int] = None) -> None:
313
+ """`<base>/rgb/set` へ直接publishする(応答は待たない)"""
314
+ if any(not (0 <= c <= 255) for c in (r, g, b)):
315
+ raise ValueError("RGB values must be between 0 and 255")
316
+ payload: Dict[str, Any] = {"state": "ON", "color": {"r": r, "g": g, "b": b}}
317
+ if brightness is not None:
318
+ payload["brightness"] = brightness
319
+ self._client.publish(f"{self.base}/rgb/set", json.dumps(payload))
320
+
321
+ def publish_led_off(self) -> None:
322
+ """`<base>/rgb/set` へ消灯を直接publishする(応答は待たない)"""
323
+ self._client.publish(f"{self.base}/rgb/set", json.dumps({"state": "OFF"}))
324
+
325
+ def publish_led_mode(self, mode: Literal["status", "manual"]) -> None:
326
+ """`<base>/led_mode/set` へ直接publishする(応答は待たない)"""
327
+ if mode not in ("status", "manual"):
328
+ raise ValueError('mode must be "status" or "manual"')
329
+ self._client.publish(f"{self.base}/led_mode/set", mode)
330
+
331
+ def publish_oled(self, text: str) -> None:
332
+ """`<base>/oled/set` へ直接publishする(応答は待たない)"""
333
+ self._client.publish(f"{self.base}/oled/set", text)
334
+
335
+ # ------------------------------------------------------------------
336
+ def close(self):
337
+ """MQTT接続を終了する"""
338
+ self._client.loop_stop()
339
+ self._client.disconnect()
340
+
341
+ def __del__(self):
342
+ try:
343
+ self.close()
344
+ except Exception:
345
+ pass
@@ -0,0 +1,19 @@
1
+ class ESP32IOError(Exception):
2
+ """esp32io_mqtt ライブラリの基底例外"""
3
+ pass
4
+
5
+ class ESP32IODeviceError(ESP32IOError):
6
+ """ESP32 が status=error を返したときの例外 (ロジックエラー)"""
7
+ pass
8
+
9
+ class ESP32IOMqttError(ESP32IOError, RuntimeError):
10
+ """MQTTブローカーとの接続・通信に失敗したときの例外"""
11
+ pass
12
+
13
+ class ESP32IOTimeoutError(ESP32IOMqttError):
14
+ """cmd/result 応答の待機がタイムアウトしたときの例外"""
15
+ pass
16
+
17
+ class ESP32IOProtocolError(ESP32IOError, RuntimeError):
18
+ """デバイスから不正な JSON や予期しない応答が返ったときの例外"""
19
+ pass
@@ -0,0 +1,68 @@
1
+ """ESP32IO-MQTT プロトコル定義
2
+
3
+ ベーストピックは `<topic_root>/<device_id>` (既定 topic_root="esp32io")。
4
+ 汎用JSONコマンドチャネルは `<base>/cmd` に送信し、`<base>/cmd/result` で応答を受け取る。
5
+ 個別エンティティのトピックは ENTITY_TOPICS を参照。
6
+ """
7
+
8
+ DEFAULT_TOPIC_ROOT = "esp32io"
9
+
10
+ # --- 汎用JSONコマンドチャネル (esp32io/<device_id>/cmd -> cmd/result) ---
11
+ CMD_PING = "ping"
12
+ CMD_GET_STATUS = "get_status"
13
+ CMD_GET_IO_STATE = "get_io_state"
14
+ CMD_READ_DI = "read_di"
15
+ CMD_SET_DO = "set_do"
16
+ CMD_READ_ADC = "read_adc"
17
+ CMD_SET_PWM = "set_pwm"
18
+ CMD_GET_PWM_CONFIG = "get_pwm_config"
19
+ CMD_SET_PWM_CONFIG = "set_pwm_config"
20
+ CMD_SET_RGB = "set_rgb"
21
+ CMD_LED_OFF = "led_off"
22
+ CMD_SET_LED_MODE = "set_led_mode"
23
+ CMD_GET_LED_STATE = "get_led_state"
24
+ CMD_HELP = "help"
25
+ CMD_I2C_SCAN = "i2c_scan"
26
+ CMD_I2C_READ = "i2c_read"
27
+ CMD_I2C_WRITE = "i2c_write"
28
+ CMD_GET_SENSORS = "get_sensors"
29
+ CMD_SET_OLED = "set_oled"
30
+
31
+ ALL_COMMANDS = (
32
+ CMD_PING,
33
+ CMD_GET_STATUS,
34
+ CMD_GET_IO_STATE,
35
+ CMD_READ_DI,
36
+ CMD_SET_DO,
37
+ CMD_READ_ADC,
38
+ CMD_SET_PWM,
39
+ CMD_GET_PWM_CONFIG,
40
+ CMD_SET_PWM_CONFIG,
41
+ CMD_SET_RGB,
42
+ CMD_LED_OFF,
43
+ CMD_SET_LED_MODE,
44
+ CMD_GET_LED_STATE,
45
+ CMD_HELP,
46
+ CMD_I2C_SCAN,
47
+ CMD_I2C_READ,
48
+ CMD_I2C_WRITE,
49
+ CMD_GET_SENSORS,
50
+ CMD_SET_OLED,
51
+ )
52
+
53
+ # --- 個別エンティティ (Home Assistant MQTT Discovery互換) のトピック購読パターン ---
54
+ # "<base>" を実際のベーストピックに置換して subscribe/publish する。
55
+ ENTITY_SUBSCRIBE_TOPICS = (
56
+ "{base}/status",
57
+ "{base}/di/+/state",
58
+ "{base}/do/+/state",
59
+ "{base}/adc/+/state",
60
+ "{base}/pwm/+/state",
61
+ "{base}/rgb/state",
62
+ "{base}/led_mode/state",
63
+ "{base}/oled/state",
64
+ "{base}/bme280/#",
65
+ "{base}/mpu6050/#",
66
+ "{base}/vl53l1x/#",
67
+ "{base}/diag/#",
68
+ )
File without changes
@@ -0,0 +1,233 @@
1
+ Metadata-Version: 2.4
2
+ Name: esp32io-mqtt
3
+ Version: 0.1.0
4
+ Summary: A lightweight Python MQTT client for controlling ESP32IO-MQTT devices (Home Assistant compatible).
5
+ Author: Noritama-Lab
6
+ License: MIT
7
+ Project-URL: Repository, https://github.com/Noritama-Lab/esp32io-mqtt-api
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: System :: Hardware
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: paho-mqtt<2.0,>=1.6
23
+ Dynamic: license-file
24
+
25
+ # ESP32IO-MQTT Python MQTT Client
26
+
27
+ ESP32IO-MQTT is a lightweight Python **MQTT** client for controlling [ESP32IO-MQTT](https://github.com/Noritama-Lab/esp32io-mqtt-firmware) devices.
28
+
29
+ 日本語 / English
30
+
31
+ ---
32
+
33
+ ## 日本語
34
+
35
+ ESP32IO-MQTT Pythonクライアントは、**MQTTブローカー** 経由で ESP32IO-MQTT ファームウェアを制御するための軽量Pythonクライアントです。
36
+
37
+ ESP32IO-MQTT ファームウェアはデバイス制御を **MQTT** 経由で行います。本クライアントもMQTT専用です。
38
+
39
+ ---
40
+
41
+ ## 特徴
42
+
43
+ - MQTTブローカー経由での接続のみをサポート(HTTP/シリアルは非対応)
44
+ - 汎用JSONコマンドチャネル (`<base>/cmd` → `<base>/cmd/result`) を使った同期API (`read_di()`, `set_do()`, `get_io_state()` 等)
45
+ - 個別エンティティのトピック (Home Assistant MQTT Discovery互換) にも対応
46
+ - `get_cached_state()` で直近受信した状態のスナップショットを取得
47
+ - `on_state_change()` で状態変化のコールバックを登録
48
+ - `publish_do()` / `publish_pwm()` / `publish_rgb()` / `publish_led_off()` / `publish_led_mode()` / `publish_oled()` で応答待ちなしに直接publish
49
+ - I2Cスキャン、リード/ライト、BME280/MPU6050/VL53L1Xなどのセンサー一括取得、OLED表示をサポート
50
+ - PWM周波数・分解能の取得/更新に対応
51
+ - 通信エラー・タイムアウト・プロトコル不整合・デバイスエラーなど統一例外を定義
52
+ - 任意JSONを送れる低レベルAPI `command()` を提供
53
+
54
+ ---
55
+
56
+ ## 動作環境
57
+
58
+ - Python 3.8 以上
59
+ - `paho-mqtt>=1.6,<2.0`
60
+ - MQTTブローカー (Mosquitto等)
61
+ - ESP32IO-MQTT ファームウェア(MQTT対応)
62
+
63
+ ---
64
+
65
+ ## インストール
66
+
67
+ ```bash
68
+ pip install -r requirements.txt
69
+ # または
70
+ pip install -e .
71
+ ```
72
+
73
+ ---
74
+
75
+ ## クイックスタート
76
+
77
+ ```python
78
+ from esp32io_mqtt import ESP32S3IOMqtt
79
+
80
+ # DEVICE_ID は設定ポータル (http://<device_ip>/) のページタイトルや
81
+ # mDNSホスト名 (ESP32IO_MQTT_XXXXXX) で確認できます。
82
+ with ESP32S3IOMqtt("ESP32IO_MQTT_XXXXXX", "192.168.1.5", debug=False) as esp:
83
+ print("ping =", esp.ping())
84
+ print("di0 =", esp.read_di(0))
85
+ print("adc0 =", esp.read_adc(0))
86
+ print("pwm config =", esp.get_pwm_config())
87
+
88
+ esp.set_do(0, 1)
89
+ esp.set_pwm(0, 128)
90
+
91
+ print(esp.get_io_state())
92
+ ```
93
+
94
+ サンプル実行:
95
+
96
+ ```bash
97
+ py -m examples.mqtt_example
98
+ ```
99
+
100
+ ---
101
+
102
+ ## 通信方式
103
+
104
+ ### 1. 汎用JSONコマンドチャネル (同期API)
105
+
106
+ `command()` および `read_di()` / `set_do()` などの高レベルメソッドは、`<base>/cmd` にJSONをpublishし `<base>/cmd/result` の応答を待つ同期方式です。戻り値やエラー(`ESP32IODeviceError`)で結果を確認できます。
107
+
108
+ ### 2. 個別エンティティのトピック (Home Assistant MQTT Discovery互換)
109
+
110
+ 接続時に `di/+/state` `do/+/state` `pwm/+/state` `rgb/state` `led_mode/state` `oled/state` `bme280/#` `mpu6050/#` `vl53l1x/#` `diag/#` `status` を自動的にsubscribeし、受信するたびに内部キャッシュへ保存します。
111
+
112
+ - `get_cached_state()` — 直近に受信した値のスナップショット (dict) を取得
113
+ - `on_state_change(callback)` — 状態トピックを受信するたびに `callback(topic_suffix, payload)` を呼び出す
114
+ - `is_online` — LWTトピック (`<base>/status`) が `online` かどうか
115
+ - `publish_do(pin_id, value)` / `publish_pwm(pin_id, duty)` / `publish_rgb(r, g, b, brightness)` / `publish_led_off()` / `publish_led_mode(mode)` / `publish_oled(text)` — 応答を待たずに直接publish(Home Assistant側からの操作と同じ経路)
116
+
117
+ ---
118
+
119
+ ## API 一覧(主要メソッド)
120
+
121
+ - `ESP32S3IOMqtt(device_id, host, port=1883, user=None, password=None, topic_root="esp32io", keepalive=30, cmd_timeout=5.0, connect_timeout=10.0, debug=False)`
122
+ - `ping()`
123
+ - `read_di(pin_id)` / `set_do(pin_id, value)`
124
+ - `read_adc(pin_id)`
125
+ - `set_pwm(pin_id, duty)`
126
+ - `get_pwm_config()` / `set_pwm_config(freq, res)`
127
+ - `set_led_mode(mode)` / `set_rgb(r, g, b, brightness)` / `led_off()` / `get_led_state()`
128
+ - `i2c_scan()` / `i2c_read(addr, length)` / `i2c_write(addr, data)`
129
+ - `get_sensors()`
130
+ - `set_oled(text, x, y, size, clear)`
131
+ - `get_io_state()` / `get_status()`
132
+ - `command(cmd, **kwargs)`
133
+ - `help()`
134
+ - `get_cached_state()` / `on_state_change(callback)` / `is_online`
135
+ - `publish_do()` / `publish_pwm()` / `publish_rgb()` / `publish_led_off()` / `publish_led_mode()` / `publish_oled()`
136
+ - `close()`
137
+
138
+ ---
139
+
140
+ ## 例外
141
+
142
+ - `ESP32IOError`(基底クラス)
143
+ - `ESP32IOMqttError` — ブローカー接続・publish失敗
144
+ - `ESP32IOTimeoutError` — `cmd/result` 応答待ちのタイムアウト(`ESP32IOMqttError`のサブクラス)
145
+ - `ESP32IOProtocolError` — デバイスから不正な応答
146
+ - `ESP32IODeviceError` — デバイスが `status=error` を返した場合
147
+
148
+ ---
149
+
150
+ ## プロジェクト構成
151
+
152
+ ```text
153
+ .
154
+ ├── esp32io_mqtt/
155
+ │ ├── __init__.py
156
+ │ ├── client.py
157
+ │ ├── exceptions.py
158
+ │ └── protocol.py
159
+ ├── examples/
160
+ │ ├── mqtt_example.py
161
+ │ └── i2c_example.py
162
+ ├── pyproject.toml
163
+ ├── requirements.txt
164
+ ├── LICENSE
165
+ └── README.md
166
+ ```
167
+
168
+ ---
169
+
170
+ ## ファームウェア
171
+
172
+ 対応ファームウェア: [ESP32IO-MQTT](https://github.com/Noritama-Lab/esp32io-mqtt-firmware)(MQTT + Home Assistant MQTT Discovery対応)。
173
+ トピック構成・汎用JSONコマンド一覧は同リポジトリの `COMMAND_REFERENCE.md` を参照してください。
174
+
175
+ ---
176
+
177
+ ## ライセンス
178
+
179
+ MIT License
180
+ Copyright (c) 2026 Noritama-Lab
181
+
182
+ ---
183
+
184
+ ## English
185
+
186
+ ESP32IO-MQTT Python client is a lightweight **MQTT** client for controlling [ESP32IO-MQTT](https://github.com/Noritama-Lab/esp32io-mqtt-firmware) firmware.
187
+
188
+ ESP32IO-MQTT firmware controls devices over **MQTT**. This client is MQTT-only.
189
+
190
+ ### Features
191
+
192
+ - MQTT broker connection only (no HTTP/Serial support)
193
+ - Synchronous API over the generic JSON command channel (`<base>/cmd` → `<base>/cmd/result`) (`read_di()`, `set_do()`, `get_io_state()`, etc.)
194
+ - Support for individual entity topics (Home Assistant MQTT Discovery compatible)
195
+ - `get_cached_state()` for a snapshot of the latest received state
196
+ - `on_state_change()` to register a callback for state changes
197
+ - `publish_do()` / `publish_pwm()` / `publish_rgb()` / `publish_led_off()` / `publish_led_mode()` / `publish_oled()` to publish directly without waiting for a response
198
+ - I2C scanning, raw read/write, sensor batch reads (BME280/MPU6050/VL53L1X), and OLED control
199
+ - PWM frequency/resolution read & update
200
+ - Unified exceptions for MQTT, protocol, timeout, and device errors
201
+ - Low-level `command()` API for custom JSON commands
202
+
203
+ ### Requirements
204
+
205
+ - Python 3.8+
206
+ - `paho-mqtt>=1.6,<2.0`
207
+ - An MQTT broker (e.g. Mosquitto)
208
+ - ESP32IO-MQTT firmware (MQTT-based)
209
+
210
+ ### Installation
211
+
212
+ ```bash
213
+ pip install -r requirements.txt
214
+ # or
215
+ pip install -e .
216
+ ```
217
+
218
+ ### Quick Start
219
+
220
+ ```python
221
+ from esp32io_mqtt import ESP32S3IOMqtt
222
+
223
+ with ESP32S3IOMqtt("ESP32IO_MQTT_XXXXXX", "192.168.1.5", debug=False) as esp:
224
+ print("ping =", esp.ping())
225
+ print("di0 =", esp.read_di(0))
226
+ esp.set_do(0, 1)
227
+ print(esp.get_io_state())
228
+ ```
229
+
230
+ ### License
231
+
232
+ MIT License
233
+ Copyright (c) 2026 Noritama-Lab
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ esp32io_mqtt/__init__.py
5
+ esp32io_mqtt/client.py
6
+ esp32io_mqtt/exceptions.py
7
+ esp32io_mqtt/protocol.py
8
+ esp32io_mqtt/py.typed
9
+ esp32io_mqtt.egg-info/PKG-INFO
10
+ esp32io_mqtt.egg-info/SOURCES.txt
11
+ esp32io_mqtt.egg-info/dependency_links.txt
12
+ esp32io_mqtt.egg-info/requires.txt
13
+ esp32io_mqtt.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ paho-mqtt<2.0,>=1.6
@@ -0,0 +1 @@
1
+ esp32io_mqtt
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "esp32io-mqtt"
7
+ version = "0.1.0"
8
+ description = "A lightweight Python MQTT client for controlling ESP32IO-MQTT devices (Home Assistant compatible)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Noritama-Lab"},
14
+ ]
15
+ dependencies = [
16
+ "paho-mqtt>=1.6,<2.0",
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 3 - Alpha",
20
+ "Intended Audience :: Developers",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.8",
25
+ "Programming Language :: Python :: 3.9",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Topic :: System :: Hardware",
30
+ ]
31
+
32
+ [project.urls]
33
+ Repository = "https://github.com/Noritama-Lab/esp32io-mqtt-api"
34
+
35
+ [tool.setuptools.packages.find]
36
+ include = ["esp32io_mqtt"]
37
+ exclude = ["examples*", "tests*"]
38
+
39
+ [tool.setuptools.package-data]
40
+ esp32io_mqtt = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+