自宅に導入されている 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 は複数ありうる。
{
"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 コンポーネントで利用している定数を定義している。
"""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 のみを含むリスト)に設定する。
"""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/ )。
"""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桁までしか含まれていない。
次のスクリーンショットのオレンジ色の楕円で示された部分の情報を抽出して回路の今日の電力量を取得している。
"""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 を設定し、日本語への変換などが適切に行えるようにした。
"""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マネシステムに応じて修正をする必要がある。
"""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マネシステムに応じて修正をする必要がある。
{
"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ファイルを作成する必要がある。
{
"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」 に記録している。












