自宅に導入されている Panasonic のHEMS (Home Energy Management System)の一種である ECOマネシステム(電気・ガス・水 計測タイプ)(生産中止品)の情報を Home Assistant に取り込むための Custom Component を作成した。
以前、「ECOマネから基本的な情報のHome Assistantでの取得」において、Home Assistant の configuration.yaml への記述で ECOマネシステムの情報を取り込むことを試みたが、多くの情報を取り込むためには configuration.yamlの記述が多くなって、取り扱いが難しいので、Custom Component を作成することにした。
ECOマネシステムからの情報取得
ECOマネシステムからは API が提供されていない。Echonet Lite の機能が(経済産業省のHEMS補助対象機器になった関係で?) アップグレードのような形で付加されたが、ちょっと調べた範囲では、電気量すら取得できないという残念なレベルの極めて限定的なものであった。このため、Echonet Lite では必要な情報が取得できない。
ただ、ECOマネシステムは、Webブラウザで電気量等を表示する機能は持っているので、Webページを解析することで、情報を取得することはできる。
Webページをスクレイピングして情報を取得するカスタムコンポーネントを作成した。
custom_components/ecomane
カスタムコンポーネントとして ecomane というセンサーコンポーネントを作成した。
https://github.com/kunsen-an/ha-eco-mane にアップロードしてある。
コンポーネント全体に関連したファイル
manifest.json
ecomane コンポーネントの manifest.json は以下の通り。
( https://developers.home-assistant.io/docs/creating_integration_manifest )
domain は、Integration を識別する名前である。同じ Integration に属する entity は複数ありうる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
{ "domain": "ecomane", "name": "Panasonic Eco Mane HEMS", "version": "0.0.4", "codeowners": ["@kunsen-an"], "config_flow": true, "dependencies": [], "documentation": "https://github.com/kunsen-an/ha_eco_mane", "homekit": {}, "iot_class": "cloud_polling", "requirements": [], "ssdp": [], "zeroconf": [], "loggers": ["custom_components.ecomane"] } |
const.py
ecomane コンポーネントで利用している定数を定義している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
"""Constants for the Eco Mane HEMS integration.""" from __future__ import annotations from homeassistant.const import Platform DOMAIN = "ecomane" DEFAULT_ENCODING = "UTF-8" # デフォルトエンコーディング DEFAULT_NAME = "Panasonic Eco Mane HEMS" DEFAULT_IP_ADDRESS = "192.168.1.220" ENCODING = "shift-jis" # ECOマネのエンコーディング PLATFORMS = [Platform.SENSOR] PLATFORM = Platform.SENSOR ENTITY_NAME = "EcoManeHEMS" # Config セレクタ CONFIG_SELECTOR_IP = "ip" CONFIG_SELECTOR_NAME = "name" # キー KEY_IP_ADDRESS = "ip_address" # 本日の使用量 SENSOR_TODAY_CGI = "ecoTopMoni.cgi" # 回路 SENSOR_CIRCUIT_CGI = "elecCheck_6000.cgi?disp=2" SENSOR_CIRCUIT_SELECTOR_PREFIX = "ojt" SENSOR_CIRCUIT_PREFIX = "em_circuit" SENSOR_CIRCUIT_SELECTOR_PLACE = "txt" SENSOR_CIRCUIT_SELECTOR_CIRCUIT = "txt2" SENSOR_CIRCUIT_SELECTOR_BUTTON = "btn btn_58" # 回路別電力 SENSOR_CIRCUIT_SELECTOR_POWER = "num" SENSOR_CIRCUIT_POWER_SERVICE_TYPE = "power" # 回路別電力量 SENSOR_CIRCUIT_ENERGY_CGI = "resultGraphDiv_4242.cgi" SENSOR_CIRCUIT_ENERGY_SELECTOR = "ttx_01" SENSOR_CIRCUIT_ENERGY_SERVICE_TYPE = "energy" # 時間間隔 RETRY_INTERVAL = 120 # 再試行間隔: 120秒 POLLING_INTERVAL = 60 # ECOマネへのpolling間隔: 60秒 |
Integration の Config
Ecomane コンポーネントを Integration として Home Assistant に登録する際の Configuration 関連のファイルは以下の通り。
__init__.py
ecomane モジュールの初期化 __init__.py では、config entry を(非同期で)設定する async_setup_entry()、(非同期で) config entry を unload する async_unload_entry() を定義している ( https://developers.home-assistant.io/docs/config_entries_index/ )。
Home Assistant のコンポーネントの startup の際に async_setup_entry() が呼び出される。
async_setup_entry では、データ更新のためのコーディネーター EcoManeDataCoordinator を作成し、ECOマネシステムからどのようなエントリが存在するかという初期データを取得する。Home Assistant のデータ構造 (hass.data)にコーディネータを保存する。
config entry を async_forward_entry_setups() でプラットフォーム (ここのPLATFORMSは const.py で定義されており、センサーを意味する Platform.SENSOR のみを含むリスト)に設定する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
"""The Eco Mane HEMS integration.""" import logging from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from .const import CONFIG_SELECTOR_IP, DOMAIN, PLATFORMS from .coordinator import EcoManeDataCoordinator _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up ecomane from a config entry.""" ip = config_entry.data[CONFIG_SELECTOR_IP] # DataCoordinatorを作成 coordinator = EcoManeDataCoordinator(hass, ip) # 初期データ取得 await coordinator.async_config_entry_first_refresh() if not coordinator.last_update_success: raise ConfigEntryNotReady("async_config_entry_first_refresh() failed") # データを hass.data に保存 hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator _LOGGER.debug("__init__.py config_entry.entry_id: %s", config_entry.entry_id) # エンティティの追加 await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) # 正常にセットアップ出来たら True を返却 return True async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" # エンティティのアンロード unload_ok = await hass.config_entries.async_forward_entry_unload( config_entry, Platform.SENSOR ) # クリーンアップ処理 if unload_ok: # EcoManeDataCoordinatorを削除 hass.data[DOMAIN].pop(config_entry.entry_id, None) return unload_ok |
config_flow.py
コンポーネントをユーザインタフェースを利用して登録できるようにするために config entry を生成する config_flow.py を定義している( https://developers.home-assistant.io/docs/config_entries_config_flow_handler/ )。
config_flow.py では、登録済みのエントリを取得する configured_instances() と EcoManeConfigFlow を定義している。
EcoManeConfigFlow では、async_step_user() によって、ユーザの入力 user_input を処理する。
user_input に辞書形式のデータが与えられている場合は、そのデータに基づいてasync_create_entry() によってエントリを作成する( https://developers.home-assistant.io/docs/data_entry_flow_index/ )。
引数 user_input が None の場合には、 async_show_form() によって、ユーザ入力を受け付けて、その入力に基づいてエントリを作成する。
ユーザ入力の際に利用されるフォームの形式は、 data_schema で定義している。必要な項目 (vol.Required)として、名前 (CONFIG_SELECTOR_NAME = "name") と IPアドレス (CONFIG_SELECTOR_IP="ip")が指定されている( https://developers.home-assistant.io/docs/data_entry_flow_index/ )。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
"""The Eco Mane Config Flow.""" import logging from typing import Any import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.core import HomeAssistant, callback from .const import ( CONFIG_SELECTOR_IP, CONFIG_SELECTOR_NAME, DEFAULT_IP_ADDRESS, DEFAULT_NAME, DOMAIN, ) _LOGGER = logging.getLogger(__name__) @callback def configured_instances(hass: HomeAssistant) -> set[str]: """Return a set of configured instances.""" return {entry.data["name"] for entry in hass.config_entries.async_entries(DOMAIN)} class EcoManeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Eco Mane.""" VERSION = 0 MINOR_VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" _LOGGER.debug("async_step_user") errors = {} if user_input is not None: # ユーザ入力の検証 if user_input[CONFIG_SELECTOR_NAME] in configured_instances(self.hass): # 既に同じ名前のエントリが存在する場合はエラー errors["base"] = "name_exists" else: # エントリを作成 return self.async_create_entry( title=user_input[CONFIG_SELECTOR_NAME], data=user_input ) # ユーザ入力フォームのスキーマ data_schema = vol.Schema( { vol.Required( CONFIG_SELECTOR_NAME, default=DEFAULT_NAME, ): str, vol.Required(CONFIG_SELECTOR_IP, default=DEFAULT_IP_ADDRESS): str, } ) # ユーザ入力フォームを表示 return self.async_show_form( step_id="user", data_schema=data_schema, errors=errors, ) |
ECOマネシステムからのデータ取得
ECOマネシステムから取得するデータは3種類に分類できる。
- 回路別の電力(W)
-
- プレフィックス EcoManeCircuitPower
-
- 回路別の電力量(kWh)
-
- プレフィックス EcoManeCircuitEnergy
-
- 全体使用量(電力量、水、ガス)
-
- プレフィックス EcoManeUsage
- 購入電気量
- 太陽光発電量
- ガス消費量
- 水消費量
- CO2排出量
- CO2削減量
- 売電量
-
Webページからのscrapingには、beautiful soup 4 を用いた。
coordinator.py
ECOマネシステムからのデータ取得を制御するコーディネータ EcoManeDataCoordinator を定義している。
EcoManeDataCoordinator では、_async_update_data()によってデータ更新を行う。_async_update_data() は、update_usage_data() と update_circuit_power_data() を呼び出している。
update_usage_data()は、全体使用量のデータを更新する。全体使用量は、回路数などの影響を受けないので個々のECOマネシステム環境に固有のURLである f"http://{self._ip_address}/{SENSOR_TODAY_CGI}" のWebページを取得し、そのページの内容を ecomane_usage_sensors_descs の情報に基づいて解析して、全体使用量を取得する。ここで、SENSOR_TODAY_CGI = "ecoTopMoni.cgi" である。
ここのURLに限らないが、ECOマネシステムのWebページのエンコーディングはshift-jis なので、ページデータを取得する際にはそれを指定する必要がある。
update_circuit_power_data()は、回路別の電力のデータを更新する。また、回路別の電力量を取得するためには、どのような電力センサーがあるかが必要なため、このupdate_circuit_power_data()のなかから、update_circuit_energy_data() を呼び出して、回路別の電力量を取得している。
update_circuit_energy_data()では、URLがf"http://{self._ip_address}/{SENSOR_CIRCUIT_ENERGY_CGI}?page={page_num}&maxp={total_page}&disp=0&selNo={selNo}&check=2" のページを取得し、そのページから対象の回路の今日の電力量を取得している。ここで、SENSOR_CIRCUIT_ENERGY_CGI = "resultGraphDiv_4242.cgi" である。
ECOマネシステムの内部的にはもう少し細かい精度で電力量が管理されていると思うが、Webページでは、小数点以下2桁までしか含まれていない。
次のスクリーンショットのオレンジ色の楕円で示された部分の情報を抽出して回路の今日の電力量を取得している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 |
"""Coordinator for Eco Mane HEMS component.""" import asyncio from collections.abc import Generator from dataclasses import dataclass from datetime import timedelta import logging import aiohttp from bs4 import BeautifulSoup, NavigableString from bs4.element import Tag from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntityDescription, SensorStateClass, ) from homeassistant.const import UnitOfEnergy, UnitOfMass, UnitOfVolume from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( ENTITY_NAME, KEY_IP_ADDRESS, POLLING_INTERVAL, RETRY_INTERVAL, SENSOR_CIRCUIT_CGI, SENSOR_CIRCUIT_ENERGY_CGI, SENSOR_CIRCUIT_ENERGY_SELECTOR, SENSOR_CIRCUIT_PREFIX, SENSOR_CIRCUIT_SELECTOR_BUTTON, SENSOR_CIRCUIT_SELECTOR_CIRCUIT, SENSOR_CIRCUIT_SELECTOR_PLACE, SENSOR_CIRCUIT_SELECTOR_POWER, SENSOR_CIRCUIT_SELECTOR_PREFIX, SENSOR_TODAY_CGI, ) _LOGGER = logging.getLogger(__name__) # 電力センサーのエンティティのディスクリプション @dataclass(frozen=True, kw_only=True) class EcoManeCircuitPowerSensorEntityDescription(SensorEntityDescription): """Describes EcoManePower sensor entity.""" service_type: str # 電力量センサーのエンティティのディスクリプション @dataclass(frozen=True, kw_only=True) class EcoManeCircuitEnergySensorEntityDescription(SensorEntityDescription): """Describes EcoManeEnergy sensor entity.""" service_type: str # 使用量センサーのエンティティのディスクリプション @dataclass(frozen=True, kw_only=True) class EcoManeUsageSensorEntityDescription(SensorEntityDescription): """Describes EcoManeEnergy sensor entity.""" description: str # 使用量センサーのエンティティのディスクリプションのリストを作成 ecomane_usage_sensors_descs = [ EcoManeUsageSensorEntityDescription( name="electricity_purchased", translation_key="electricity_purchased", description="Electricity purchased 購入電気量", key="num_L1", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), EcoManeUsageSensorEntityDescription( name="solar_power_energy", translation_key="solar_power_energy", description="Solar Power Energy / 太陽光発電量", key="num_L2", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), EcoManeUsageSensorEntityDescription( name="gas_consumption", translation_key="gas_consumption", description="Gas Consumption / ガス消費量", key="num_L4", device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, state_class=SensorStateClass.TOTAL_INCREASING, ), EcoManeUsageSensorEntityDescription( name="water_consumption", translation_key="water_consumption", description="Water Consumption / 水消費量", key="num_L5", device_class=SensorDeviceClass.WATER, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, state_class=SensorStateClass.TOTAL_INCREASING, ), EcoManeUsageSensorEntityDescription( name="co2_emissions", translation_key="co2_emissions", description="CO2 Emissions / CO2排出量", key="num_R1", device_class=SensorDeviceClass.WEIGHT, native_unit_of_measurement=UnitOfMass.KILOGRAMS, state_class=SensorStateClass.TOTAL_INCREASING, ), EcoManeUsageSensorEntityDescription( name="co2_reduction", translation_key="co2_reduction", description="CO2 Reduction / CO2削減量", key="num_R2", device_class=SensorDeviceClass.WEIGHT, native_unit_of_measurement=UnitOfMass.KILOGRAMS, state_class=SensorStateClass.TOTAL_INCREASING, ), EcoManeUsageSensorEntityDescription( name="electricity_sales", translation_key="electricity_sales", description="Electricity sales / 売電量", key="num_R3", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), ] class EcoManeDataCoordinator(DataUpdateCoordinator): """EcoMane Data coordinator.""" _attr_circuit_total: int # 総回路数 _attr_usage_sensor_descs: list[EcoManeUsageSensorEntityDescription] _data_dict: dict[str, str] def __init__(self, hass: HomeAssistant, ip_address: str) -> None: """Initialize my coordinator.""" super().__init__( hass, _LOGGER, name=ENTITY_NAME, update_interval=timedelta( seconds=POLLING_INTERVAL ), # data polling interval ) self._data_dict = {KEY_IP_ADDRESS: ip_address} self._session = None self._circuit_count = 0 self._ip_address = ip_address self._attr_circuit_total = 0 self._attr_usage_sensor_descs = ecomane_usage_sensors_descs def natural_number_generator(self) -> Generator: """Natural number generator.""" count = 1 while True: yield count count += 1 async def _async_update_data(self) -> dict[str, str]: """Update Eco Mane Data.""" _LOGGER.debug("_async_update_data: Updating EcoMane data") # debug await self.update_usage_data() await self.update_circuit_power_data() return self._data_dict async def update_usage_data(self) -> None: """Update usage data.""" _LOGGER.debug("update_usage_data") try: # デバイスからデータを取得 url = f"http://{self._ip_address}/{SENSOR_TODAY_CGI}" async with aiohttp.ClientSession() as session: response: aiohttp.ClientResponse = await session.get(url) if response.status != 200: _LOGGER.error( "Error fetching data from %s. Status code: %s", url, response.status, ) raise UpdateFailed( f"Error fetching data from {url}. Status code: {response.status}" ) # テキストデータを取得する際にエンコーディングを指定 text_data = await response.text(encoding="shift-jis") await self.parse_usage_data(text_data) _LOGGER.debug("EcoMane usage data updated successfully") except Exception as err: _LOGGER.error("Error updating usage data: %s", err) raise UpdateFailed("update_usage_data failed") from err # finally: async def parse_usage_data(self, text: str) -> dict: """Parse data from the content.""" # BeautifulSoupを使用してHTMLを解析 soup = BeautifulSoup(text, "html.parser") # 指定したIDを持つdivタグの値を取得して辞書に格納 for usage_sensor_desc in ecomane_usage_sensors_descs: key = usage_sensor_desc.key div = soup.find("div", id=key) if div: value = div.text.strip() self._data_dict[key] = value return self._data_dict async def update_circuit_power_data(self) -> dict: """Update power data.""" _LOGGER.debug("update_circuit_power_data") try: # デバイスからデータを取得 url = f"http://{self._ip_address}/{SENSOR_CIRCUIT_CGI}" async with aiohttp.ClientSession() as session: self._circuit_count = 0 for ( page_num ) in self.natural_number_generator(): # 1ページ目から順に取得 url = f"http://{self._ip_address}/{SENSOR_CIRCUIT_CGI}&page={page_num}" response: aiohttp.ClientResponse = await session.get(url) if response.status != 200: _LOGGER.error( "Error fetching data from %s. Status code: %s", url, response.status, ) raise UpdateFailed( f"Error fetching data from {url}. Page: {page_num} Status code: {response.status}" ) # テキストデータを取得する際に shift-jis エンコーディングを指定 text_data = await response.text(encoding="shift-jis") # text_data からデータを取得, 最大ページ total_page に達したら終了 total_page = await self.parse_circuit_power_data( text_data, page_num ) if page_num >= total_page: break self._attr_circuit_total = self._circuit_count _LOGGER.debug("Total number of circuits: %s", self._attr_circuit_total) except Exception as err: _LOGGER.error("Error updating circuit power data: %s", err) raise UpdateFailed("update_circuit_power_data failed") from err # finally: _LOGGER.debug("EcoMane circuit power data updated successfully") return self._data_dict async def parse_circuit_power_data(self, text: str, page_num: int) -> int: """Parse data from the content.""" # BeautifulSoupを使用してHTMLを解析 soup = BeautifulSoup(text, "html.parser") # 最大ページ数を取得 maxp = soup.find("input", {"name": "maxp"}) total_page = 0 if isinstance(maxp, Tag): value = maxp.get("value", "0") # Ensure value is a string before converting to int if isinstance(value, str): total_page = int(value) # ページ内の各センサーエンティティのデータを取得 for button_num in range(1, 9): sensor_num = self._circuit_count prefix = f"{SENSOR_CIRCUIT_PREFIX}_{sensor_num:02d}" div_id = f"{SENSOR_CIRCUIT_SELECTOR_PREFIX}_{button_num:02d}" # ojt_?? div_element: Tag | NavigableString | None = soup.find("div", id=div_id) if isinstance(div_element, Tag): # 回路の(ボタンの)selNo button_div = div_element.find( "div", class_=SENSOR_CIRCUIT_SELECTOR_BUTTON, # btn btn_58 ) # btn btn_58 if isinstance(button_div, Tag): a_tag = button_div.find("a") # <a href="javascript:moveCircuitChange('selNo')">...</a> if isinstance(a_tag, Tag) and "href" in a_tag.attrs: href_value = a_tag["href"] # JavaScriptの関数呼び出しを分解 if isinstance(href_value, str): js_parts = href_value.split("moveCircuitChange('") if len(js_parts) > 1: selNo = js_parts[1].split("')")[0] self._data_dict[ f"{prefix}_{SENSOR_CIRCUIT_SELECTOR_BUTTON}" ] = selNo # 場所 element: Tag | NavigableString | int | None = div_element.find( "div", class_=SENSOR_CIRCUIT_SELECTOR_PLACE, # txt ) if isinstance(element, Tag): self._data_dict[f"{prefix}_{SENSOR_CIRCUIT_SELECTOR_PLACE}"] = ( element.get_text() ) # txt # 回路 element = div_element.find( "div", class_=SENSOR_CIRCUIT_SELECTOR_CIRCUIT ) # txt2 if isinstance(element, Tag): self._data_dict[f"{prefix}_{SENSOR_CIRCUIT_SELECTOR_CIRCUIT}"] = ( element.get_text() ) # txt2 # 電力 element = div_element.find( "div", class_=SENSOR_CIRCUIT_SELECTOR_POWER ) # num if isinstance(element, Tag): self._data_dict[f"{prefix}_{SENSOR_CIRCUIT_SELECTOR_POWER}"] = ( element.get_text().split("W")[0] ) # 電力量を取得 await self.update_circuit_energy_data( page_num, total_page, selNo, prefix ) # 回路数をカウント self._circuit_count += 1 # デバッグログ _LOGGER.debug( "page:%s id:%s prefix:%s selNo:%s circuit_power:%s circuit_energy:%s", page_num, div_id, prefix, selNo, self._data_dict[f"{prefix}_{SENSOR_CIRCUIT_SELECTOR_POWER}"], self._data_dict[f"{prefix}_{SENSOR_CIRCUIT_ENERGY_SELECTOR}"], ) else: _LOGGER.debug("div_element not found div_id:%s", div_id) break return total_page async def update_circuit_energy_data( self, page_num: int, total_page: int, selNo: str, prefix: str ) -> None: """Update circuit energy data.""" _LOGGER.debug( "update_circuit_energye_data page_num:%s total_page:%s selNo:%s prefix:%s", page_num, total_page, selNo, prefix, ) try: # デバイスからデータを取得 url = f"http://{self._ip_address}/{SENSOR_CIRCUIT_ENERGY_CGI}?page={page_num}&maxp={total_page}&disp=0&selNo={selNo}&check=2" async with aiohttp.ClientSession() as session: response: aiohttp.ClientResponse = await session.get(url) if response.status != 200: _LOGGER.error( "Error fetching data from %s. Status code: %s", url, response.status, ) raise UpdateFailed( f"Error fetching data from {url}. Status code: {response.status}" ) # テキストデータを取得する際にエンコーディングを指定 text_data = await response.text(encoding="shift-jis") # 回路別電力量を取得 circuit_energy = await self.parse_circuit_energy_data(text_data, prefix) _LOGGER.debug( "EcoMane circuit energy data updated successfully. circuit_energy:%f", circuit_energy, ) except Exception as err: _LOGGER.error("Error updating circuit energy data: %s", err) raise UpdateFailed("update_circuit_energy_data failed") from err # finally: async def parse_circuit_energy_data(self, text: str, prefix: str) -> float: """Parse data from the content.""" # BeautifulSoupを使用してHTMLを解析 soup = BeautifulSoup(text, "html.parser") # 今日の消費電力量を取得 (<div id="ttx_01" class="ttx">今日:1.02kWh 昨日:3.16kWh</div>) ttx = soup.find("div", id=SENSOR_CIRCUIT_ENERGY_SELECTOR) # ttx_01 if isinstance(ttx, Tag): today_parts = ttx.get_text().split("今日:") if len(today_parts) > 1: today_energy = today_parts[1].split("kWh")[0] sensor_id = f"{prefix}_{SENSOR_CIRCUIT_ENERGY_SELECTOR}" self._data_dict[sensor_id] = today_energy _LOGGER.debug( "prefix:%s circuit_energy:%s", prefix, self._data_dict[sensor_id], ) return float(today_energy) async def async_config_entry_first_refresh(self) -> None: """Perform the first refresh with retry logic.""" while True: try: self.data = await self._async_update_data() break except UpdateFailed as err: _LOGGER.warning( "Initial data fetch failed, retrying in %d seconds: %s", RETRY_INTERVAL, err, ) await asyncio.sleep(RETRY_INTERVAL) # Retry interval @property def circuit_total(self) -> int: """Total number of power sensors.""" return self._attr_circuit_total @property def usage_sensor_descs(self) -> list[EcoManeUsageSensorEntityDescription]: """Usage sensor descriptions.""" return self._attr_usage_sensor_descs @property def ip_address(self) -> str: """IP address.""" return self._data_dict[KEY_IP_ADDRESS] |
sensor.py
config entry に基づいて、sensor entity を設定するための async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) が定義されている。この中で、async_add_entities(sensors, update_before_add=False) の update_before_add=False は、オーバービューに自動でエントリが登録され、表示されてしまわないようにするためである。この執筆時点ではupdate_before_addをTrue にすると、オーバービューに自動で登録される。自動登録は、update_before_addの元来の意味とは異なるので将来的には何か変更があるかもしれない。
ECOマネシステムのデータをSensor Entityとして提供する EcoManeUsageSensorEntity、 EcoManeCircuitPowerSensorEntity、 EcoManeCircuitEnergySensorEntity が定義されている。
EcoManeUsageSensorEntityは、(全体の)使用量を示すセンサー、 EcoManeCircuitPowerSensorEntityは回路別の電力を示すセンサー、 EcoManeCircuitEnergySensorEntityは回路別の電力量を示すセンサーである。
センサーのエンティティには translation_key を設定し、日本語への変換などが適切に行えるようにした。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 |
"""The Eco Mane Sensor.""" from __future__ import annotations import logging from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntity, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfEnergy, UnitOfPower from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( DOMAIN, SENSOR_CIRCUIT_ENERGY_SELECTOR, SENSOR_CIRCUIT_ENERGY_SERVICE_TYPE, SENSOR_CIRCUIT_POWER_SERVICE_TYPE, SENSOR_CIRCUIT_PREFIX, SENSOR_CIRCUIT_SELECTOR_CIRCUIT, SENSOR_CIRCUIT_SELECTOR_PLACE, SENSOR_CIRCUIT_SELECTOR_POWER, ) from .coordinator import ( EcoManeCircuitEnergySensorEntityDescription, EcoManeCircuitPowerSensorEntityDescription, EcoManeDataCoordinator, EcoManeUsageSensorEntityDescription, ) from .name_to_id import ja_to_entity _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensor entities from a config entry.""" # Access data stored in hass.data coordinator: EcoManeDataCoordinator = hass.data[DOMAIN][config_entry.entry_id] sensor_dict = coordinator.data power_sensor_total = coordinator.circuit_total ecomane_energy_sensors_descs = coordinator.usage_sensor_descs sensors: list[SensorEntity] = [] _LOGGER.debug("sensor.py async_setup_entry sensors: %s", sensors) # 使用量センサーのエンティティのリストを作成 for usage_sensor_desc in ecomane_energy_sensors_descs: sensor = EcoManeUsageSensorEntity(coordinator, usage_sensor_desc) sensors.append(sensor) # 電力センサーのエンティティのリストを作成 for sensor_num in range(power_sensor_total): prefix = f"{SENSOR_CIRCUIT_PREFIX}_{sensor_num:02d}" place = sensor_dict[f"{prefix}_{SENSOR_CIRCUIT_SELECTOR_PLACE}"] circuit = sensor_dict[f"{prefix}_{SENSOR_CIRCUIT_SELECTOR_CIRCUIT}"] _LOGGER.debug( "sensor.py async_setup_entry sensor_num: %s, prefix: %s, place: %s, circuit: %s", sensor_num, prefix, place, circuit, ) sensors.append( EcoManeCircuitPowerSensorEntity(coordinator, prefix, place, circuit) ) sensors.append( EcoManeCircuitEnergySensorEntity(coordinator, prefix, place, circuit) ) # センサーが見つからない場合はエラー if not sensors: raise ConfigEntryNotReady("No sensors found") # エンティティを追加 (update_before_add=False でオーバービューに自動で登録されないようにする) async_add_entities(sensors, update_before_add=False) _LOGGER.debug("sensor.py async_setup_entry has finished async_add_entities") class EcoManeUsageSensorEntity(CoordinatorEntity, SensorEntity): """EcoMane UsageS ensor.""" _attr_has_entity_name = True # _attr_name = None # Noneでも値を設定するとtranslationがされない _attr_unique_id: str | None = None _attr_attribution = "Usage data provided by Panasonic ECO Mane HEMS" _attr_entity_description: EcoManeUsageSensorEntityDescription | None = None _attr_device_class: SensorDeviceClass | None = None _attr_state_class: str | None = None _attr_native_unit_of_measurement: str | None = None _attr_div_id: str = "" _attr_description: str | None = None _ip_address: str | None = None def __init__( self, coordinator: EcoManeDataCoordinator, usage_sensor_desc: EcoManeUsageSensorEntityDescription, ) -> None: """Pass coordinator to CoordinatorEntity.""" super().__init__(coordinator=coordinator) # ip_address を設定 self._ip_address = coordinator.ip_address # 使用量 sensor_id (_attr_div_id) を設定 sensor_id = usage_sensor_desc.key self._attr_div_id = sensor_id # 使用量 entity_description を設定 self._attr_entity_description = usage_sensor_desc self._attr_description = description = usage_sensor_desc.description # 使用量 translation_key を設定 self._attr_translation_key = usage_sensor_desc.translation_key # 使用量 entity_id を設定 self.entity_id = f"{SENSOR_DOMAIN}.{DOMAIN}_{usage_sensor_desc.translation_key}" # 使用量 _attr_unique_id を設定 if ( coordinator is not None and coordinator.config_entry is not None and description is not None ): self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{usage_sensor_desc.translation_key}" # 使用量 device_class, state_class, native_unit_of_measurement を設定 self._attr_device_class = usage_sensor_desc.device_class self._attr_state_class = usage_sensor_desc.state_class self._attr_native_unit_of_measurement = ( usage_sensor_desc.native_unit_of_measurement ) # デバッグログ _LOGGER.debug( "usage_sensor_desc.name: %s, _attr_translation_key: %s, _attr_div_id: %s, entity_id: %s, _attr_unique_id: %s", usage_sensor_desc.name, self._attr_translation_key, self._attr_div_id, self.entity_id, self._attr_unique_id, ) @property def native_value(self) -> str: """State.""" value = self.coordinator.data.get(self._attr_div_id) # 使用量 if value is None: return "" return str(value) @property def device_info( self, ) -> DeviceInfo: # エンティティ群をデバイスに分類するための情報を提供 """Return the device info.""" ip_address = self._ip_address return DeviceInfo( # 使用量のデバイス情報 identifiers={(DOMAIN, "daily_usage_" + (ip_address or ""))}, name="Daily Usage", manufacturer="Panasonic", translation_key="daily_usage", ) class EcoManeCircuitPowerSensorEntity(CoordinatorEntity, SensorEntity): """EcoManePowerSensor.""" _attr_has_entity_name = True # _attr_name = None # Noneでも値を設定するとtranslationがされない _attr_unique_id: str | None = None _attr_attribution = "Power data provided by Panasonic ECO Mane HEMS" _attr_entity_description: EcoManeCircuitPowerSensorEntityDescription | None = None _attr_device_class = SensorDeviceClass.POWER _attr_state_class = SensorStateClass.MEASUREMENT _attr_native_unit_of_measurement = UnitOfPower.WATT _attr_sensor_id: str _ip_address: str | None = None def __init__( self, coordinator: EcoManeDataCoordinator, prefix: str, place: str, circuit: str, ) -> None: """Pass coordinator to CoordinatorEntity.""" super().__init__(coordinator=coordinator) # ip_address を設定 self._ip_address = coordinator.ip_address # 回路別電力 sensor_id を設定 sensor_id = f"{prefix}_{SENSOR_CIRCUIT_SELECTOR_POWER}" # num self._attr_sensor_id = sensor_id # 回路別電力 entity_description を設定 self._attr_entity_description = description = ( EcoManeCircuitPowerSensorEntityDescription( service_type=SENSOR_CIRCUIT_POWER_SERVICE_TYPE, key=sensor_id, ) ) # 回路 translation_key を設定 name = f"{place} {circuit}" self._attr_translation_key = ja_to_entity(name) # 回路別電力量 entity_id を設定 self.entity_id = ( f"{SENSOR_DOMAIN}.{DOMAIN}_{sensor_id}_{self._attr_translation_key}" ) # 回路別電力量 _attr_unique_id を設定 if ( coordinator is not None and coordinator.config_entry is not None and description is not None ): self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.service_type}_{description.key}" @property def native_value(self) -> str: """State.""" value = self.coordinator.data.get(self._attr_sensor_id) # 回路別電力 if value is None: return "" return str(value) @property def device_info( self, ) -> DeviceInfo: # エンティティ群をデバイスに分類するための情報を提供 """Return the device info.""" ip_address = self._ip_address return DeviceInfo( # 回路別電力のデバイス情報 identifiers={(DOMAIN, "power_consumption_" + (ip_address or ""))}, name="Power Consumption", manufacturer="Panasonic", translation_key="power_consumption", ) class EcoManeCircuitEnergySensorEntity(CoordinatorEntity, SensorEntity): """EcoManeCircuitEnergySensor.""" _attr_has_entity_name = True # _attr_name = None # Noneでも値を設定するとtranslationがされない _attr_unique_id: str | None = None _attr_attribution = "Power data provided by Panasonic ECO Mane HEMS" _attr_entity_description: EcoManeCircuitEnergySensorEntityDescription | None = None _attr_device_class = SensorDeviceClass.ENERGY _attr_state_class = SensorStateClass.TOTAL_INCREASING _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR _attr_sensor_id: str _ip_address: str | None = None def __init__( self, coordinator: EcoManeDataCoordinator, prefix: str, place: str, circuit: str, ) -> None: """Pass coordinator to CoordinatorEntity.""" super().__init__(coordinator=coordinator) # ip_address を設定 self._ip_address = coordinator.ip_address # 回路別電力量 sensor_id を設定 sensor_id = f"{prefix}_{SENSOR_CIRCUIT_ENERGY_SELECTOR}" # ttx_01 self._attr_sensor_id = sensor_id # 回路別電力量 entity_description を設定 self._attr_entity_description = description = ( EcoManeCircuitPowerSensorEntityDescription( service_type=SENSOR_CIRCUIT_ENERGY_SERVICE_TYPE, key=sensor_id, ) ) # 回路 translation_key を設定 name = f"{place} {circuit}" self._attr_translation_key = ja_to_entity(name) # 回路別電力量 entity_id を設定 self.entity_id = ( f"{SENSOR_DOMAIN}.{DOMAIN}_{sensor_id}_{self._attr_translation_key}" ) # 回路別電力量 _attr_unique_id を設定 if ( coordinator is not None and coordinator.config_entry is not None and description is not None ): self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.service_type}_{description.key}" @property def native_value(self) -> str: """State.""" value = self.coordinator.data.get(self._attr_sensor_id) # 回路別電力量 if value is None: return "" return str(value) @property def device_info( self, ) -> DeviceInfo: # エンティティ群をデバイスに分類するための情報を提供 """Return the device info.""" ip_address = self._ip_address return DeviceInfo( # 回路別電力量のデバイス情報 identifiers={(DOMAIN, "energy_consumption_" + (ip_address or ""))}, name="Energy Consumption", manufacturer="Panasonic", translation_key="energy_consumption", ) |
名前の翻訳関係
ECOマネシステムに登録してある回路名とecomaneコンポーネントで利用する(Home Assistant 内部で利用する)名前、Home Assistant で表示される名前の変換のためにname_to_id.py、strings.json、translations/ja.jsonがある。
name_to_id.py
ECOマネシステムには回路名などを日本語で登録してあるが、それをそのまま Home Assistant内部で利用すると、漢字部分が中国語読みのようなローマ字に変換されてしまい、何を意味しているのか分かりづらい。そこで、ECOマネシステムのWebページ内に含まれている回路名をHome Assistant 内部で利用する英単語を連結したIDに変換するために、name_to_id.py を定義した。name_to_id.py は、実際に利用するECOマネシステムに応じて修正をする必要がある。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
"""Creating a translation dictionary.""" # 日本語からエンティティ名への変換辞書 ja_to_entity_translation_dict = { "購入電気量": "electricity_purchased", "CO2削減量": "co2_reduction", "CO2排出量": "co2_emissions", "ガス消費量": "gas_consumption", "太陽光発電量": "solar_power_energy", "売電量": "electricity_sales", " 太陽光": "solar_panel", "1階トイレ コンセント": "1f_toilet_outlets", "キッチン 照明&コンセント": "kitchen_lighting_and_outlets", "キッチン 食器洗い乾燥機": "kitchen_dishwasher", "キッチン(下) コンセント": "kitchen_lower_outlets", "キッチン(上) コンセント": "kitchen_upper_outlets", "シャワー洗面納戸 照明&コンセント": "shower_lighting_and_outlets", "ダイニング エアコン": "dining_air_conditioner", "ダイニング 照明&コンセント": "dining_lighting_and_outlets", "ダイニング(南) 照明&コンセント": "dining_south_lighting_and_outlets", "ダイニング(北) コンセント": "dining_north_outlets", "リビング エアコン": "living_air_conditioner", "リビング 非常警報設備": "living_alarm_system", "リビング(南) 照明&コンセント": "living_south_lighting_and_outlets", "リビング(北) 照明&コンセント": "living_north_lighting_and_outlets", "階段・2階共用 照明&コンセント": "stairs_2_common_lighting_and_outlets", "玄関・1階廊下 照明&コンセント": "foyer_lighting_and_outlets", "書斎 エアコン": "den_air_conditioner", "書斎 照明&コンセント": "den_lighting_and_outlets", "寝室 エアコン": "bedroom_air_conditioner", "寝室・クロゼット 照明&コンセント": "bedroom_closet_lighting_and_outlets", "寝室(東) 照明&コンセント": "bedroom_east_lighting_and_outlets", "水消費量": "water_consumption", "洗面 照明&コンセント": "washroom_lighting_and_outlets", "洗面 洗濯機": "washroom_washing_machine", "洗面 暖房器": "washroom_heater", "洋室1父母部屋 エアコン": "room1_air_conditioner", "洋室1父母部屋 照明&コンセント": "room1_lighting_and_outlets", "洋室2 エアコン": "room2_air_conditioner", "洋室2 照明&コンセント": "room2_lighting_and_outlets", "洋室3子供部屋 エアコン": "room3_air_conditioner", "洋室3子供部屋 照明&コンセント": "room3_lighting_and_outlets", "浴室 浴室乾燥機": "bathroom_dryer", } # 日本語名をエンティティ名に変換 def ja_to_entity(name: str) -> str: """Translate Japanese name to entity name.""" return ja_to_entity_translation_dict.get(name, name) |
strings.json
内部のIDを表示名にするための標準を strings.json に定義した。name_to_id.pyによる変換結果の内部のIDの影響を受けるので、実際に利用するECOマネシステムに応じて修正をする必要がある。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
{ "title": "ECO Mane HEMS Integration", "config": { "step": { "user": { "title": "Configure Eco Mane HEMS", "description": "Please fill out the following information to set up the integration.", "data": { "name": "Name", "ip": "IP" }, "data_description": { "name": "Enter a unique name for this integration instance.", "ip": "Provide the IP address to fetch data from." } }, "confirm": { "description": "Do you want to set up {name}?" } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, "device": { "daily_usage": { "name": "Today's Usage" }, "power_consumption": { "name": "Electric Power Consumption" }, "energy_consumption": { "name": "Today's Electric Energy Consumption" } }, "entity": { "sensor": { "electricity_purchased": { "name": "Eletricity Purchased" }, "solar_power_energy": { "name": "Solar Power Energy" }, "gas_consumption": { "name": "Gas Consumption" }, "water_consumpotion": { "name": "Water Consumption" }, "co2_emissions": { "name": "CO2 Emissions" }, "co2_reduction": { "name": "CO2 Reduction" }, "electricity_sales": { "name": "Electricity Sales" }, "solar_power": { "name": "Solar Power" }, "1f_toilet_outlets": { "name": "1F Toilet Outlets" }, "kitchen_lighting_and_outlets": { "name": "Kitchen Lighting & Outlets" }, "kitchen_dishwasher": { "name": "Kitchen Dishwasher" }, "kitchen_lower_outlets": { "name": "Kitchen (Lower) Outlets" }, "kitchen_upper_outlets": { "name": "Kitchen (Upper) Outlets" }, "shower_lighting_and_outlets": { "name": "Shower Lighting & Outlets" }, "dining_air_conditioner": { "name": "Dining Air Conditioner" }, "dining_lighting_and_outlets": { "name": "Dining Lighting & Outlets" }, "dining_south_lighting_and_outlets": { "name": "Dining South Lighting & Outlets" }, "dining_north_outlets": { "name": "Dining North Outlets" }, "living_air_conditioner": { "name": "Living Air Conditioner" }, "living_alarm_system": { "name": "Living Alarm System" }, "living_south_lighting_and_outlets": { "name": "Lining South Lighting & Outlets" }, "living_north_lighting_and_outlets": { "name": "Living North Lighting & Outlets" }, "stairs_2_common_lighting_and_outlets": { "name": "Stairs & 2nd Floor Common Lighting & Outlets" }, "foyer_lighting_and_outlets": { "name": "Foyle Lighting & Outlets" }, "den_air_conditioner": { "name": "Den Air Conditioner" }, "den_lighting_and_outlets": { "name": "Den Lighting & Outlets" }, "bedroom_air_conditioner": { "name": "Bedroom Air Conditioner" }, "bedroom_closet_lighting_and_outlets": { "name": "Bedroom Closet Lighting & Outlets" }, "bedroom_east_lighting_and_outlets": { "name": "Bedroom East Lighting & Outlets" }, "water_consumption": { "name": "Water Consumption" }, "washroom_lighting_and_outlets": { "name": "Washroom Lighting & Outlets" }, "washroom_washing_machine": { "name": "Washtoom Washing Machine" }, "washroom_heater": { "name": "Washroom Heater" }, "room1_air_conditioner": { "name": "Room1 Air Conditioner" }, "room1_lighting_and_outlets": { "name": "Room1 Lighting & Outlets" }, "room2_air_conditioner": { "name": "Room2 Air Conditioner" }, "room2_lighting_and_outlets": { "name": "Room2 Lighting & Outlets" }, "room3_air_conditioner": { "name": "Room3 Air Conditioner" }, "room3_lighting_and_outlets": { "name": "Room3 Lighting & Outlets" }, "bathroom_dryer": { "name": "Bathroom Dryer" } } } } |
translations/ja.json
内部のIDを日本語の表示名にするための例を translations/ja.json に示す。これも、実際に利用するECOマネシステムに応じて修正をする必要がある。また、他言語への変換が必要な場合には、その言語に応じて .jsonファイルを作成する必要がある。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
{ "title": "ECOマネHEMS", "config": { "abort": { "already_configured": "デバイスはすでに設定済み" }, "error": { "cannot_connect": "接続に失敗", "invalid_auth": "無効な認証", "unknown": "予期していないエラー" }, "step": { "user": { "title": "ECOマネの設定", "description": "統合の設定に必要な以下の情報を設定してください.", "data": { "name": "名称", "ip": "IPアドレス" }, "data_description": { "name": "名称を指定してください.", "ip": "測定値を取得するIPアドレスを指定していください." } }, "confirm": { "description": "{name}を設定しますか?" } } }, "device": { "daily_usage": { "name": "今日の使用量" }, "power_consumption": { "name": "消費電力" }, "energy_consumption": { "name": "今日の消費電力量" } }, "entity": { "sensor": { "electricity_purchased": { "name": "購入電気量", "state": "{value} kWh", "attributes": { "unit_of_measurement": "kWh", "translation_placeholders": { "value": { "description": "エネルギー消費量" } } } }, "solar_power_energy": { "name": "太陽光発電量" }, "gas_consumption": { "name": "ガス消費量" }, "water_consumpotion": { "name": "水消費量" }, "co2_emissions": { "name": "CO2排出量", "state": "{value} kg", "attributes": { "unit_of_measurement": "kg", "translation_placeholders": { "value": { "description": "CO2量" } } } }, "co2_reduction": { "name": "CO2削減量" }, "electricity_sales": { "name": "売電量" }, "solar_panel": { "name": "太陽光パネル" }, "1f_toilet_outlets": { "name": "1階トイレ コンセント" }, "kitchen_lighting_and_outlets": { "name": "キッチン 照明&コンセント" }, "kitchen_dishwasher": { "name": "キッチン 食器洗い乾燥機" }, "kitchen_lower_outlets": { "name": "キッチン(下) コンセント" }, "kitchen_upper_outlets": { "name": "キッチン(上) コンセント" }, "shower_lighting_and_outlets": { "name": "シャワー洗面納戸 照明&コンセント" }, "dining_air_conditioner": { "name": "ダイニング エアコン" }, "dining_lighting_and_outlets": { "name": "ダイニング 照明&コンセント" }, "dining_south_lighting_and_outlets": { "name": "ダイニング(南) 照明&コンセント" }, "dining_north_outlets": { "name": "ダイニング(北) コンセント" }, "living_air_conditioner": { "name": "リビング エアコン" }, "living_alarm_system": { "name": "リビング 非常警報設備" }, "living_south_lighting_and_outlets": { "name": "リビング(南) 照明&コンセント" }, "living_north_lighting_and_outlets": { "name": "リビング(北) 照明&コンセント" }, "stairs_2_common_lighting_and_outlets": { "name": "階段・2階共用 照明&コンセント" }, "foyer_lighting_and_outlets": { "name": "玄関・1階廊下 照明&コンセント" }, "den_air_conditioner": { "name": "書斎 エアコン" }, "den_lighting_and_outlets": { "name": "書斎 照明&コンセント" }, "bedroom_air_conditioner": { "name": "寝室 エアコン" }, "bedroom_closet_lighting_and_outlets": { "name": "寝室・クロゼット 照明&コンセント" }, "bedroom_east_lighting_and_outlets": { "name": "寝室(東) 照明&コンセント" }, "water_consumption": { "name": "水消費量" }, "washroom_lighting_and_outlets": { "name": "洗面 照明&コンセント" }, "washroom_washing_machine": { "name": "洗面 洗濯機" }, "washroom_heater": { "name": "洗面 暖房器" }, "room1_air_conditioner": { "name": "洋室1父母部屋 エアコン" }, "room1_lighting_and_outlets": { "name": "洋室1父母部屋 照明&コンセント" }, "room2_air_conditioner": { "name": "洋室2 エアコン" }, "room2_lighting_and_outlets": { "name": "洋室2 照明&コンセント" }, "room3_air_conditioner": { "name": "洋室3子供部屋 エアコン" }, "room3_lighting_and_outlets": { "name": "洋室3子供部屋 照明&コンセント" }, "bathroom_dryer": { "name": "浴室 浴室乾燥機" } } } } |
Home Assistantでの利用
ecomane のインストール
まず、ecomane を Home Assistant にインストールする必要がある。homeassistantのディレクトリの config/custom_components/ の下にインストールすることでIntegrationとして登録可能になる。
手動でのインストール
GitHub の https://github.com/kunsen-an/ha-eco-mane からダウンロードして、ファイルを修正する必要がある。
https://github.com/kunsen-an/ha-eco-maneのトップが、homeassistantのディレクトリの config/custom_components/ha-eco-mane になるようダウンロードする。
以下のファイルの内容をECOマネシステムに登録してある回路名に合わせて修正する。
- name_to_id.py
- strings.json
- translations/ja.json
また、const.py の DEFAULT_IP_ADDRESS も修正しておけば、Home Assistant への登録時にデフォルトとして利用できる。
HACSを利用したインストール
実際には、name_to_id.py、strings.json、translations/ja.json のECOマネシステムに合わせた修正してある リポジトリがあれば、Home Assistant Community Store (HACS) を利用してインストールすることが可能である。
HACSは Home Assistant にインストール済みとする。
まず、HACSを開いて、右上の3点メニューから Custom repositories を選択する。
Custom repositories のパネルに、リポジトリのURL (https://github.com/kunsen-an/ha-eco-mane)を入力し、Type に Integration を選択肢、ADD をクリックする。
HACSで検索できるようになったので、 ecomane で検索をする。
検索結果の ecomane の3点メニューから download を選択する。
HACS に正式に登録されていないので警告が表示されるので、確認をして、Downloadをクリックする(以下はテスト用環境でのスクリーンショットのため、ディレクトリが本番環境とは異なる)。Home Assistant の config/custom_components/ecomane にダウンロードされる。
ダウンロードが完了したら、Home Assistant の再起動が必要である。
Integration としての登録
インストールした ecomane を Integration として登録することで、ECOマネシステムから情報を取得して Home Assistant で表示したりできるようになる。
統合へのECOマネの追加
「+統合を追加」を選択し、「ecomane」を検索する。
Panasonic Eco Mane HEMS が見つかったら、それを選択する。ユーザ入力フォーム「ECOマネの設定」が表示されるので、設置環境に合わせて、名称とECOマネのIPアドレスを設定し「送信」する。
うまく設定されれば、統合に「ECOマネHEMS」が追加される。
ダッシュボードでのデバイスの表示
sensor.py の async_add_entities(sensors, update_before_add=False) の update_before_add=Falseによって、オーバービューに自動登録されなくなっているので、ダッシュボードで表示するためには表示のためのカード追加やエネルギーの設定等の設定が必要である。
オーバービューでの表示
ecomaneのIntegrationに含まれる3種類のデバイス(今日の使用量、消費電力、今日の消費電力量)をオーバービューに追加して(一部を)表示すると次のようになる。消費電力、今日の消費電力量には分電盤の回路数分のエンティティが含まれる。
エネルギーでの表示
デバイス「今日の使用量」のエンティティをエネルギーのページに設定することで、次のような表示を得ることができる。ECOマネシステムの小さなディスプレイよりも広い領域を使うことでわかりやすく表示される。
ECOマネシステムのネットワーク
別記事に書いたが、ECOマネシステムのネットワークは癖が強く、ECOマネシステムが受信するパケットが(ブロードキャストやマルチキャストであっても)少し多くなるとポート遮断を行い、しばらく通信ができなくなる。この問題に対処するためにセグメントを分けてトラフィックを少なくするようにしていたが、トラフィックがまだ多い場合がありポート遮断が頻発するようになっていた。また、IPアドレスがXX.YY.ZZ.220に固定されており、同一セグメントからでないと HTTP request に対する responseが受け取れない謎仕様である。この謎仕様のために、ECOマネシステムだけのセグメントを作って、トラフィックを減らすということをするためには、別セグメントからアクセスできるように reverse proxy を設置する必要がある。同一セグメントに他の機器をつなぎつつ、ECOマネシステムのポート遮断が起きないようにするために、パケットフィルタリングをするtransparent firewall (bridge) を OpenWrt で作成し、利用することにした。パケットフィルリングについては、「OpenWrt の nftables を使用した Transparent Firewall」 に記録している。