Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions ha_addon/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,23 @@ else
echo "POWER_MULTIPLIER=$power_multiplier"
fi

# Fetch this add-on's slug from the supervisor so MQTT discovery can
# link discovered meter devices to the add-on device via_device.
addon_slug=""
if addon_slug="$(bashio::api.supervisor GET '/store/addons/self' false '.slug')" && [ -n "$addon_slug" ]; then
bashio::log.info "Resolved add-on slug for HA discovery: $addon_slug"
else
bashio::log.warning "Failed to resolve add-on slug from supervisor; meter devices will not be linked via_device"
addon_slug=""
fi

if bashio::config.has_value 'mqtt_uri'; then
bashio::log.info "Using custom MQTT broker URL from configuration"
echo ""
echo "[MQTT_INSIGHTS]"
echo "URI=$(bashio::config 'mqtt_uri')"
echo "HA_DISCOVERY=True"
[ -n "$addon_slug" ] && echo "ADDON_SLUG=$addon_slug"
elif bashio::services.available "mqtt"; then
bashio::log.info "Using Home Assistant's internal MQTT broker"
echo ""
Expand All @@ -159,6 +170,7 @@ else
echo "PASSWORD=$(bashio::services 'mqtt' 'password')"
echo "TLS=$(bashio::services 'mqtt' 'ssl')"
echo "HA_DISCOVERY=True"
[ -n "$addon_slug" ] && echo "ADDON_SLUG=$addon_slug"
fi
} > "$CONFIG"
fi
Expand Down
1 change: 1 addition & 0 deletions src/astrameter/config/config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,5 +570,6 @@ def read_mqtt_insights_config(
section, "HA_DISCOVERY_PREFIX", fallback=""
)
or "homeassistant",
addon_slug=config.get(section, "ADDON_SLUG", fallback="") or None,
)
return None
35 changes: 24 additions & 11 deletions src/astrameter/mqtt_insights/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def build_ct002_consumer_discovery(
state_topic = f"{base_topic}/ct002/{device_id}/consumer/{consumer_id}"
avail_topic = f"{state_topic}/availability"
uid_prefix = f"astrameter_ct002_{safe_dev}_{safe_cid}"
meter_identifier = f"astrameter_ct002_{safe_dev}"

components: dict[str, dict] = {}

Expand Down Expand Up @@ -194,6 +195,7 @@ def build_ct002_consumer_discovery(
if device_type
else f"AstraMeter Consumer {mac_slug}",
"manufacturer": "Marstek",
"via_device": meter_identifier,
}
connections: list[list[str]] = []
if re.fullmatch(r"[0-9a-f]{12}", mac_slug):
Expand Down Expand Up @@ -237,6 +239,7 @@ def build_ct002_device_discovery(
base_topic: str,
device_id: str,
ha_prefix: str,
addon_slug: str | None = None,
) -> tuple[str, dict]:
safe_dev = _sanitize_id(device_id)
node_id = f"astrameter_ct002_{safe_dev}"
Expand Down Expand Up @@ -281,12 +284,16 @@ def build_ct002_device_discovery(
},
}

device_info: dict = {
"identifiers": node_id,
"name": f"AstraMeter CT002 {device_id}",
"manufacturer": "astrameter",
}
if addon_slug:
device_info["via_device"] = addon_slug

payload = {
"device": {
"identifiers": node_id,
"name": f"CT002 {device_id}",
"manufacturer": "astrameter",
},
"device": device_info,
"origin": _origin(),
"components": components,
"availability": [_system_availability(base_topic)],
Expand Down Expand Up @@ -372,8 +379,9 @@ def build_shelly_battery_discovery(
payload = {
"device": {
"identifiers": node_id,
"name": f"Shelly Battery {battery_ip}",
"name": f"AstraMeter Shelly Battery {battery_ip}",
"manufacturer": "astrameter",
"via_device": f"astrameter_shelly_{safe_dev}",
},
"origin": _origin(),
"components": components,
Expand All @@ -400,6 +408,7 @@ def build_shelly_device_discovery(
base_topic: str,
device_id: str,
ha_prefix: str,
addon_slug: str | None = None,
) -> tuple[str, dict]:
safe_dev = _sanitize_id(device_id)
node_id = f"astrameter_shelly_{safe_dev}"
Expand All @@ -417,12 +426,16 @@ def build_shelly_device_discovery(
},
}

device_info: dict = {
"identifiers": node_id,
"name": f"AstraMeter Shelly {device_id}",
"manufacturer": "astrameter",
}
if addon_slug:
device_info["via_device"] = addon_slug

payload = {
"device": {
"identifiers": node_id,
"name": f"Shelly {device_id}",
"manufacturer": "astrameter",
},
"device": device_info,
"origin": _origin(),
"components": components,
"availability": [_system_availability(base_topic)],
Expand Down
26 changes: 23 additions & 3 deletions src/astrameter/mqtt_insights/mqtt_insights_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def test_ct002_consumer_discovery_structure():
assert dev["manufacturer"] == "Marstek"
assert dev["model_id"] == "HMJ-2"
assert ["bluetooth", "AA:BB:CC:DD:EE:FF"] in dev["connections"]
assert dev["via_device"] == "astrameter_ct002_dev1"

# Check two-level availability
assert payload["availability_mode"] == "all"
Expand Down Expand Up @@ -125,11 +126,12 @@ def test_ct002_consumer_discovery_no_device_type():


def test_ct002_consumer_discovery_non_mac_consumer():
"""No connections field when consumer_id is not a 12-char hex MAC."""
"""Non-MAC consumer_id has no connections but is still linked via via_device."""
_, payload = build_ct002_consumer_discovery(
"astrameter", "dev1", "192.168.1.1:12345", "homeassistant"
)
assert "connections" not in payload["device"]
assert payload["device"]["via_device"] == "astrameter_ct002_dev1"


def test_ct002_consumer_discovery_network_mac_and_ip():
Expand All @@ -146,11 +148,16 @@ def test_ct002_consumer_discovery_network_mac_and_ip():
assert ["bluetooth", "AA:BB:CC:DD:EE:FF"] in conns
assert ["mac", "11:22:33:44:55:66"] in conns
assert ["ip", "192.168.1.10"] in conns
assert payload["device"]["via_device"] == "astrameter_ct002_dev1"


def test_ct002_device_discovery_structure():
topic, payload = build_ct002_device_discovery("astrameter", "dev1", "homeassistant")
topic, payload = build_ct002_device_discovery(
"astrameter", "dev1", "homeassistant", addon_slug="34dea19a_astrameter"
)
_assert_discovery_structure(topic, payload)
assert "AstraMeter" in payload["device"]["name"]
assert payload["device"]["via_device"] == "34dea19a_astrameter"
comps = payload["components"]
assert "smooth_target" in comps
assert "active_control" in comps
Expand All @@ -170,6 +177,8 @@ def test_shelly_battery_discovery_structure():
"astrameter", "shelly1", "192.168.1.100", "homeassistant"
)
_assert_discovery_structure(topic, payload)
assert "AstraMeter" in payload["device"]["name"]
assert payload["device"]["via_device"] == "astrameter_shelly_shelly1"
comps = payload["components"]
assert "grid_power_total" in comps
assert "active" in comps
Expand All @@ -184,12 +193,21 @@ def test_shelly_battery_discovery_structure():

def test_shelly_device_discovery_structure():
topic, payload = build_shelly_device_discovery(
"astrameter", "shelly1", "homeassistant"
"astrameter", "shelly1", "homeassistant", addon_slug="34dea19a_astrameter"
)
_assert_discovery_structure(topic, payload)
assert "AstraMeter" in payload["device"]["name"]
assert payload["device"]["via_device"] == "34dea19a_astrameter"
assert "battery_count" in payload["components"]


def test_meter_device_discovery_omits_via_device_without_addon_slug():
_, ct002 = build_ct002_device_discovery("astrameter", "dev1", "homeassistant")
assert "via_device" not in ct002["device"]
_, shelly = build_shelly_device_discovery("astrameter", "shelly1", "homeassistant")
assert "via_device" not in shelly["device"]


def test_unique_ids_are_unique():
"""All unique_ids within a single discovery payload must be distinct."""
_, payload = build_ct002_consumer_discovery(
Expand Down Expand Up @@ -288,6 +306,7 @@ def test_read_mqtt_insights_config_present():
BASE_TOPIC = my_topic
HA_DISCOVERY = true
HA_DISCOVERY_PREFIX = ha
ADDON_SLUG = 34dea19a_astrameter
"""
)
result = read_mqtt_insights_config(cfg)
Expand All @@ -300,6 +319,7 @@ def test_read_mqtt_insights_config_present():
assert result.base_topic == "my_topic"
assert result.ha_discovery is True
assert result.ha_discovery_prefix == "ha"
assert result.addon_slug == "34dea19a_astrameter"


def test_read_mqtt_insights_config_defaults():
Expand Down
5 changes: 3 additions & 2 deletions src/astrameter/mqtt_insights/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class MqttInsightsConfig:
base_topic: str = "astrameter"
ha_discovery: bool = True
ha_discovery_prefix: str = "homeassistant"
addon_slug: str | None = None


@dataclass
Expand Down Expand Up @@ -341,7 +342,7 @@ async def _handle_ct002_event(
if did not in self._discovered_ct002_devices:
self._discovered_ct002_devices.add(did)
topic, payload = build_ct002_device_discovery(
base, did, cfg.ha_discovery_prefix
base, did, cfg.ha_discovery_prefix, addon_slug=cfg.addon_slug
)
await client.publish(
topic, payload=json.dumps(payload).encode(), retain=True
Expand Down Expand Up @@ -437,7 +438,7 @@ async def _handle_shelly_event(
if did not in self._discovered_shelly_devices:
self._discovered_shelly_devices.add(did)
topic, payload = build_shelly_device_discovery(
base, did, cfg.ha_discovery_prefix
base, did, cfg.ha_discovery_prefix, addon_slug=cfg.addon_slug
)
await client.publish(
topic, payload=json.dumps(payload).encode(), retain=True
Expand Down
Loading