22# -*- coding: utf-8 -*-
33"""
44AISEG2 → MQTT Discovery (per-circuit daily kWh)
5- - ブログ記載コードと等価のスクレイピング手順
6- * 各リクエストごとに HTTPDigestAuth を付与
7- * 総量: /page/graph/52111, 53111, 54111, 51111 の <span id="val_kwh"> 先頭
8- * 回路一覧: /page/setting/installation/734 の window.onload JSON(arrayCircuitNameList, strBtnType=="1")
9- * 回路kWh: /page/graph/584?data=base64({"circuitid":"NN"}) の <span id="val_kwh"> 先頭
5+ - スクレイプ: ブログ検証コードと同等(HTTPDigestAuth、/52111/53111/54111/51111、/584?data=base64({"circuitid":"NN"}))
106- MQTT:
11- * Discovery/config は retain=True
12- * state も retain=True(初回/再起動時に確実に値付与 )
13- * 送信は publish → wait_for_publish() でACK待ち(sleep不要)
14- - エンティティID: object_id で回路番号固定( sensor.c{ID} )
15- - 単発実行想定(定期実行は HA Automation / cron 推奨)
7+ * Discovery/config: retain=True
8+ * state: retain=True(HA再起動後も直ちに当日値を復元 )
9+ * publishはACK待ち(wait_for_publish)、sleep不使用
10+ - エンティティID: object_id を "aiseg2mqtt_..." として固定(例: sensor.aiseg2mqtt_c12 )
11+ - リセット表現: state_class=total + last_reset=当日0:00(JST)
1612"""
1713
1814import os
1915import sys
2016import json
2117import base64
2218import re
23- import datetime as dt
19+ from datetime import datetime , timezone , timedelta
2420from typing import Dict , List , Any
25- from zoneinfo import ZoneInfo
2621
2722import requests
2823from requests .auth import HTTPDigestAuth
3429load_dotenv (dotenv_path = os .getenv ("AISEG2_ENV_FILE" , ".env" ))
3530
3631# ----- Settings -----
37- AISEG_HOST = os .getenv ("AISEG_HOST" , "192.168.0.216" )
38- AISEG_USER = os .getenv ("AISEG_USER" , "aiseg" )
39- AISEG_PASS = os .getenv ("AISEG_PASS" , "" )
32+ AISEG_HOST = os .getenv ("AISEG_HOST" , "192.168.0.216" )
33+ AISEG_USER = os .getenv ("AISEG_USER" , "aiseg" )
34+ AISEG_PASS = os .getenv ("AISEG_PASS" , "" )
4035
41- MQTT_HOST = os .getenv ("MQTT_HOST" , "127.0.0.1" )
42- MQTT_PORT = int (os .getenv ("MQTT_PORT" , "1883" ))
43- MQTT_USER = os .getenv ("MQTT_USER" , "" )
44- MQTT_PASS = os .getenv ("MQTT_PASS" , "" )
45- MQTT_PREFIX = os .getenv ("MQTT_PREFIX" , "homeassistant" )
36+ MQTT_HOST = os .getenv ("MQTT_HOST" , "127.0.0.1" )
37+ MQTT_PORT = int (os .getenv ("MQTT_PORT" , "1883" ))
38+ MQTT_USER = os .getenv ("MQTT_USER" , "" )
39+ MQTT_PASS = os .getenv ("MQTT_PASS" , "" )
40+ MQTT_PREFIX = os .getenv ("MQTT_PREFIX" , "homeassistant" )
4641
4742DEVICE_ID = os .getenv ("DEVICE_ID" , "aiseg2-scrape" )
4843DEVICE_NAME = os .getenv ("DEVICE_NAME" , "AISEG2 (Scraped)" )
4944MANUFACTURER = os .getenv ("MANUFACTURER" , "Panasonic" )
5045MODEL = os .getenv ("MODEL" , "AISEG2" )
5146
5247AISEG_TIMEOUT = int (os .getenv ("AISEG_TIMEOUT" , "10" ))
53- SCAN_TERM = os .getenv ("SCAN_TERM" , "day" ) # day固定想定
48+ SCAN_TERM = os .getenv ("SCAN_TERM" , "day" ) # day 固定想定
49+
50+ # ----- 時刻(JSTの当日0:00をlast_resetに使う) -----
51+ JST = timezone (timedelta (hours = 9 ))
52+ def today_reset_iso () -> str :
53+ now = datetime .now (JST )
54+ reset = now .replace (hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
55+ return reset .isoformat ()
5456
5557# ----- HTTP / Parse -----
5658def _auth () -> HTTPDigestAuth :
@@ -114,15 +116,15 @@ def fetch_circuit_kwh(circuit_id: str) -> float:
114116
115117# ----- MQTT -----
116118def mqtt_client () -> mqtt .Client :
117- mc = mqtt .Client (mqtt . CallbackAPIVersion . VERSION2 , client_id = "" , protocol = mqtt .MQTTv311 )
119+ mc = mqtt .Client (protocol = mqtt .MQTTv311 )
118120 if MQTT_USER :
119121 mc .username_pw_set (MQTT_USER , MQTT_PASS )
120122 mc .max_inflight_messages_set (100 )
121123 mc .connect (MQTT_HOST , MQTT_PORT , 10 )
122- mc .loop_start () # 送受信ループ開始( ACK処理のため必須)
124+ mc .loop_start () # ACK処理のため必須
123125 return mc
124126
125- def publish_and_wait (mc : mqtt .Client , topic : str , payload : str , qos : int = 1 , retain : bool = True ) -> None :
127+ def publish_and_wait (mc : mqtt .Client , topic : str , payload : str | None , qos : int = 1 , retain : bool = True ) -> None :
126128 """PublishしてACKを待つ(sleep不要)"""
127129 info = mc .publish (topic , payload , qos = qos , retain = retain )
128130 info .wait_for_publish ()
@@ -143,60 +145,60 @@ def energy_state_topic(uid: str, key: str) -> str:
143145def circuit_state_topic (uid : str , cid : str ) -> str :
144146 return f"{ MQTT_PREFIX } /aiseg2/{ uid } /c{ cid } /kwh/state"
145147
146- def publish_discovery_energy (mc : mqtt .Client , uid : str , key : str , name : str , object_id : str , avty_t : str , attr_t : str ) -> str :
148+ def publish_discovery_energy (mc : mqtt .Client , uid : str , key : str , name : str ,
149+ object_id : str , avty_t : str , attr_t : str , last_reset_iso : str ) -> str :
150+ """
151+ 合計系(当日使用/買電/売電/発電)。entity_id は object_id に依存。
152+ """
147153 uniq = f"{ uid } _{ key } "
148154 cfg_t = f"{ MQTT_PREFIX } /sensor/{ uniq } /config"
149155 st_t = energy_state_topic (uid , key )
150- # 日本時間の今日の0時を計算
151- jst = ZoneInfo ("Asia/Tokyo" )
152- now_jst = dt .datetime .now (jst )
153- today_midnight_jst = now_jst .replace (hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
154-
155156 payload = {
156157 "name" : name ,
157- "object_id" : f"aiseg2mqtt_{ object_id } " , # ← prefix追加
158+ "object_id" : f"aiseg2mqtt_{ object_id } " , # sensor.aiseg2mqtt_total_today 等
158159 "uniq_id" : uniq ,
159160 "dev" : {"ids" :[uid ], "name" : DEVICE_NAME , "mf" : MANUFACTURER , "mdl" : MODEL , "sw" : "web-scrape" },
160- "stat_t" : st_t , "avty_t" : avty_t ,
161+ "stat_t" : st_t ,
162+ "avty_t" : avty_t ,
161163 "dev_cla" : "energy" ,
162- "stat_cla" : "total" ,
164+ "stat_cla" : "total" , # デイリーリセットの累積値
165+ "last_reset" : last_reset_iso ,
163166 "unit_of_meas" : "kWh" ,
164167 "val_tpl" : "{{ value_json.kwh }}" ,
165168 "json_attr_t" : attr_t ,
166- "last_reset" : today_midnight_jst .isoformat (),
167169 }
168170 publish_and_wait (mc , cfg_t , json .dumps (payload , ensure_ascii = False ))
169171 return st_t
170172
171- def publish_discovery_circuit (mc : mqtt .Client , uid : str , cid : str , cname : str , avty_t : str ) -> str :
173+ def publish_discovery_circuit (mc : mqtt .Client , uid : str , cid : str , cname : str ,
174+ avty_t : str , last_reset_iso : str ) -> str :
175+ """
176+ 回路別(当日kWh)。entity_id は sensor.aiseg2mqtt_c{cid} に固定。
177+ """
172178 uniq = f"{ uid } _c{ cid } _kwh"
173179 cfg_t = f"{ MQTT_PREFIX } /sensor/{ uniq } /config"
174180 st_t = circuit_state_topic (uid , cid )
175- # 日本時間の今日の0時を計算
176- jst = ZoneInfo ("Asia/Tokyo" )
177- now_jst = dt .datetime .now (jst )
178- today_midnight_jst = now_jst .replace (hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
179-
180181 payload = {
181182 "name" : cname ,
182- "object_id" : f"aiseg2mqtt_c{ cid } " , # ← prefix追加
183+ "object_id" : f"aiseg2mqtt_c{ cid } " ,
183184 "uniq_id" : uniq ,
184185 "dev" : {"ids" :[uid ], "name" : DEVICE_NAME , "mf" : MANUFACTURER , "mdl" : MODEL , "sw" : "web-scrape" },
185- "stat_t" : st_t , "avty_t" : avty_t ,
186+ "stat_t" : st_t ,
187+ "avty_t" : avty_t ,
186188 "dev_cla" : "energy" ,
187189 "stat_cla" : "total" ,
190+ "last_reset" : last_reset_iso ,
188191 "unit_of_meas" : "kWh" ,
189192 "val_tpl" : "{{ value_json.kwh }}" ,
190- "json_attr_t" : st_t ,
191- "last_reset" : today_midnight_jst .isoformat (),
193+ "json_attr_t" : st_t , # state payloadを属性にも流用
192194 }
193195 publish_and_wait (mc , cfg_t , json .dumps (payload , ensure_ascii = False ))
194196 return st_t
195197
196-
197198# ----- Main -----
198199def main () -> None :
199200 uid = DEVICE_ID
201+ last_reset_iso = today_reset_iso ()
200202
201203 # 1) データ取得
202204 circuits = fetch_circuit_catalog ()
@@ -206,15 +208,14 @@ def main() -> None:
206208 # 2) MQTT接続
207209 mc = mqtt_client ()
208210
209- # 3) Availability / Meta(retain)
211+ # 3) Availability / Meta(retain=True )
210212 avty_t = availability_topic (uid )
211213 publish_and_wait (mc , avty_t , "online" )
212-
213214 meta_t = meta_topic (uid )
214- meta = {"term" : SCAN_TERM , "circuit_count" : len (per ), "ts" : dt . datetime .now ().isoformat ()}
215+ meta = {"term" : SCAN_TERM , "circuit_count" : len (per ), "ts" : datetime .now (JST ).isoformat ()}
215216 publish_and_wait (mc , meta_t , json .dumps (meta , ensure_ascii = False ))
216217
217- # 4) Discovery(config retained )
218+ # 4) Discovery(config 全件・retain=True )
218219 total_map = [
219220 ("total_use_kwh" ,"Total Energy Today" ,"total_today" ),
220221 ("buy_kwh" , "Purchased Energy Today" ,"buy_today" ),
@@ -223,15 +224,13 @@ def main() -> None:
223224 ]
224225 total_state_topics : Dict [str , str ] = {}
225226 for key , disp , obj in total_map :
226- st = publish_discovery_energy (mc , uid , key , disp , obj , avty_t , meta_t )
227+ st = publish_discovery_energy (mc , uid , key , disp , obj , avty_t , meta_t , last_reset_iso )
227228 total_state_topics [key ] = st
228229
229- circuit_state_topics : List [Dict [str , str ]] = []
230230 for c in per :
231- st = publish_discovery_circuit (mc , uid , c ["id" ], c ["name" ], avty_t )
232- circuit_state_topics .append ({"topic" : st , "id" : c ["id" ]})
231+ publish_discovery_circuit (mc , uid , c ["id" ], c ["name" ], avty_t , last_reset_iso )
233232
234- # 5) state(retained, ACK待ち )
233+ # 5) state(全件・retain=True )
235234 for key , st in total_state_topics .items ():
236235 publish_and_wait (mc , st , json .dumps ({"kwh" : totals .get (key , 0.0 )}))
237236
@@ -247,7 +246,7 @@ def main() -> None:
247246 try :
248247 main ()
249248 except Exception as e :
250- # 失敗時は availability=offline を残す(ACK待ちで確実に送る)
249+ # 失敗時は availability=offline をretainで残す
251250 try :
252251 mc = mqtt_client ()
253252 publish_and_wait (mc , availability_topic (DEVICE_ID ), "offline" )
0 commit comments