Skip to content

Gateway cleanup step 1#3708

Open
springfall2008 wants to merge 1 commit intomainfrom
gateway_fixes
Open

Gateway cleanup step 1#3708
springfall2008 wants to merge 1 commit intomainfrom
gateway_fixes

Conversation

@springfall2008
Copy link
Copy Markdown
Owner

No description provided.

Copilot AI review requested due to automatic review settings March 31, 2026 08:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is a first cleanup step for the Gateway MQTT integration, shifting gateway telemetry publishing toward HA-style entities with consistent attributes, and expanding tests around entity injection and command routing.

Changes:

  • Replaced the old protobuf field-path decode_telemetry() mapping with direct entity injection via _inject_entities() / _inject_inverter_entities() and a centralized GATEWAY_ATTRIBUTE_TABLE.
  • Updated the plan-executed hook conversion (_on_plan_executed) to compute gateway target SoC as a percentage using soc_max, and added gateway mode constants.
  • Significantly expanded test_gateway.py coverage for entity injection, automatic config mapping, and select/number/switch event routing.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
apps/predbat/tests/test_gateway.py Reworks/extends tests to validate _inject_entities(), automatic_config(), and event→command routing behavior.
apps/predbat/predbat.py Passes soc_max and reserve into the on_plan_executed hook for gateway consumers.
apps/predbat/gateway.py Introduces attribute table + entity injection updates, updates plan hook conversion, tightens entity-id routing, and removes decode_telemetry().

Comment on lines +196 to +203
# Apply % 24 to prevent hour overflow for overnight windows (e.g. end=1485 → 00:45)
start_hour = start_minutes // 60
start_minute = start_minutes % 60
end_hour = end_minutes // 60
end_minute = end_minutes % 60
# Skip zero-duration windows (start == end after midnight wrap)
if start_hour == end_hour and start_minute == end_minute:
self.log(f"Warn: GatewayMQTT: Skipping zero-duration charge window {i} ({start_minutes}-{end_minutes})")
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code/comment says “Apply % 24 to prevent hour overflow” for overnight windows, but start_hour/end_hour are computed with integer division only (no modulo). If PredBat provides an overnight end minute > 1440 (e.g. 1485), end_hour becomes 24+ which is an invalid hour for the gateway schedule and also breaks the zero-duration detection after wrap. Apply modulo 24 (and run the zero-duration check after wrapping) before building the plan entry.

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +55
# Time options for schedule select entities (HH:MM:SS, one per minute across 24 h)
_GATEWAY_BASE_TIME = datetime.datetime.strptime("00:00", "%H:%M")
_GATEWAY_OPTIONS_TIME = [(_GATEWAY_BASE_TIME + datetime.timedelta(seconds=m * 60)).strftime("%H:%M:%S") for m in range(0, 24 * 60)]
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_GATEWAY_OPTIONS_TIME builds a 1440-entry options list and is embedded into the attributes for each schedule select. Since _inject_inverter_entities() republishes these attributes on every telemetry update, this can create very large state payloads and unnecessary churn (memory/CPU and potentially HA state/recorder bloat). Consider reducing the granularity (e.g. 5-min steps) and/or only sending the options metadata once (or behind a feature flag) instead of on every state update.

Suggested change
# Time options for schedule select entities (HH:MM:SS, one per minute across 24 h)
_GATEWAY_BASE_TIME = datetime.datetime.strptime("00:00", "%H:%M")
_GATEWAY_OPTIONS_TIME = [(_GATEWAY_BASE_TIME + datetime.timedelta(seconds=m * 60)).strftime("%H:%M:%S") for m in range(0, 24 * 60)]
# Time options for schedule select entities (HH:MM:SS, across 24 h)
# Use 5-minute steps to limit payload size for entity attributes while
# still matching the core 5-minute prediction resolution.
_GATEWAY_TIME_STEP_MINUTES = 5
_GATEWAY_BASE_TIME = datetime.datetime.strptime("00:00", "%H:%M")
_GATEWAY_OPTIONS_TIME = [
(_GATEWAY_BASE_TIME + datetime.timedelta(seconds=m * 60)).strftime("%H:%M:%S")
for m in range(0, 24 * 60, _GATEWAY_TIME_STEP_MINUTES)
]

Copilot uses AI. Check for mistakes.
Comment on lines +257 to 260
self.log(f"Info: GatewayMQTT: Plan entries ({len(plan_entries)}): " + ", ".join(f"mode={e['mode']} {e['start_hour']:02d}:{e['start_minute']:02d}-{e['end_hour']:02d}:{e['end_minute']:02d}" for e in plan_entries))

# Queue plan for async publishing (picked up by run() cycle)
if self._plan_changed(plan_entries):
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logs a full formatted list of plan entries at Info level on every plan execution. Since PredBat recalculates/executes plans frequently, this is likely to spam logs and increase IO. Recommend downgrading to Debug, or only logging when the plan actually changes (e.g. inside the _plan_changed() branch).

Suggested change
self.log(f"Info: GatewayMQTT: Plan entries ({len(plan_entries)}): " + ", ".join(f"mode={e['mode']} {e['start_hour']:02d}:{e['start_minute']:02d}-{e['end_hour']:02d}:{e['end_minute']:02d}" for e in plan_entries))
# Queue plan for async publishing (picked up by run() cycle)
if self._plan_changed(plan_entries):
# Queue plan for async publishing (picked up by run() cycle)
if self._plan_changed(plan_entries):
self.log(
"Info: GatewayMQTT: Plan entries ({}): {}".format(
len(plan_entries),
", ".join(
"mode={} {:02d}:{:02d}-{:02d}:{:02d}".format(
e["mode"],
e["start_hour"],
e["start_minute"],
e["end_hour"],
e["end_minute"],
)
for e in plan_entries
),
)
)

Copilot uses AI. Check for mistakes.
limit_kwh = charge_limits[i] if i < len(charge_limits or []) else soc_max
if limit_kwh <= 0:
continue
# XXX: If limit_khw == reserve then its a hold charge, need logic for this to be added
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling/typo in the comment: “limit_khw” should be “limit_kwh”, and “its” should be “it’s”. (Minor, but this comment is likely to be referenced when implementing the hold-charge logic.)

Suggested change
# XXX: If limit_khw == reserve then its a hold charge, need logic for this to be added
# XXX: If limit_kwh == reserve then it's a hold charge, need logic for this to be added

Copilot uses AI. Check for mistakes.
# idle_*_time should have one entry per inverter (1 in this case)
assert len(gw._args["idle_start_time"]) == 1
assert len(gw._args["idle_end_time"]) == 1
suffix = "em1234" # last 6 of "EM123456" lower-cased — actually "123456"
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This local variable is unused and the inline comment is misleading (“em1234” is not the last 6 chars of EM123456). Please remove the unused assignment and keep the comment accurate to avoid confusion when maintaining these tests.

Suggested change
suffix = "em1234" # last 6 of "EM123456" lower-cased — actually "123456"

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants