Skip to content

feat(myenergi): Zappi and Eddi monitoring, auto-config and boost controls - #4667

Merged
springfall2008 merged 27 commits into
mainfrom
feat/myenergi-component
Aug 23, 2026
Merged

feat(myenergi): Zappi and Eddi monitoring, auto-config and boost controls#4667
springfall2008 merged 27 commits into
mainfrom
feat/myenergi-component

Conversation

@springfall2008

Copy link
Copy Markdown
Owner

Adds a myenergi component providing Zappi (EV charger) and Eddi (hot water diverter) monitoring, automatic wiring of their energy sensors into the optimiser, and boost controls.

Two APIs, one interface

myenergi exposes two unrelated APIs, and Predbat needs both:

Direct ("director") 3rd-party
Host director.myenergi.net, redirecting via X_MYENERGI-asn api.s18.myenergi.net
Auth HTTP Digest — hub serial + API key, self-served at myaccount.myenergi.com OAuth2 bearer JWT
Availability Any myenergi owner, today Needs manual partner registration
Eddi Fully supported Documented as "in development"

Both sit behind a MyEnergiTransport ABC and normalise to one MyEnergiDevice dataclass, so publishing, auto-config, controls and tests are written once. myenergi_auth_method selects between them, defaulting to direct.

The cloud transport reuses oauth_mixin.py exactly as fox.py/deye.py/solis.py do. It cannot be exercised end to end until myenergi issue partner credentials; it is built against the published OpenAPI schema and unit-tested on recorded payloads.

What it does

  • Monitoring — per-device entities named sensor.predbat_myenergi_{kind}_{serial}_*, so multi-device sites work.
  • Automatic configuration (myenergi_automatic, default on) — Zappi session energy into car_charging_energy as a list (several chargers sum), plus car_charging_planned from the plug-status sensor; the first Eddi into iboost_energy_today. All via set_arg_auto(), so an explicit apps.yaml value is reported rather than silently replaced.
  • Controls — a boost switch with a companion number entity for the amount. Events queue onto the run loop rather than running on the event thread. A Zappi outside Eco/Eco+ is refused locally instead of issuing a call myenergi would reject.
  • Stubs — mode, priority, minimum green level, phase setting and schedules are documented interface stubs that warn once and return False. Libbi, webhooks, managed mode and super schedules are out of scope.
  • CLI harnesspython3 myenergi.py --hub-serial … --api-key … exercises either transport against a live account without running Predbat.

Configuration

myenergi_hub_serial: '12345678'
myenergi_api_key: 'your-api-key'

Get the key from myaccount.myenergi.com → Advanced → API Key; the hub serial is on the hub and in the app.

Notes for reviewers

  • car_charging_planned templates. The shipped templates match sensor.myenergi_zappi_[0-9a-z]+_plug_status, which targets the third-party ha-myenergi HACS integration and does not match this component's entity names — so charge planning silently fell back to the car_charging_threshold heuristic. automatic_config() now wires it, and 'ev ready to charge' (pilot states C1/D1) was added to car_charging_planned_response in the 24 templates carrying the myenergi vocabulary. It was the only connected pilot state missing while less-ready states were already accepted. Nothing under apps/predbat/ reads templates/, so existing installs are unaffected.
  • aiohttp>=3.12 is now pinned. DigestAuthMiddleware and ClientSession(middlewares=…) require it; the direct transport guards on availability with an actionable message rather than failing with a raw AttributeError.
  • myenergi_poll_seconds is capped at 30 minutes so a large value cannot trip components.py's 60-minute staleness check now that the success timestamp only advances on cycles that actually polled.

Known limitation

Session energy sensors reset to zero at session end. Predbat's incrementing-counter cleanup handles that correctly — iboost_today and car_charging_energy both come out right. However clean_incrementing_reverse only treats a drop as a reset when it exceeds ~1 kWh, so a session ending below that can be under-counted, in both figures. Documented in docs/components.md.

Testing

apps/predbat/tests/test_myenergi.py — 74 assertions covering normalisation, both transports (including every error path and the record_api_call reason labels), transport selection, publishing, auto-config, controls, the stubs, and last-good-reading retention. Registered as myenergi in TEST_REGISTRY.

Full ./run_all --quick passes.

🤖 Generated with Claude Code

springfall2008 and others added 22 commits August 23, 2026 10:29
Design for a myenergi component supporting both the direct director API and the
official 3rd party OAuth API, with Zappi and Eddi monitoring, automatic wiring of
their energy sensors, and boost controls.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Introduces the myenergi module with the shared MyEnergiDevice dataclass and
normalisers for both the direct and cloud API payload shapes, so that the
transports added next can share every layer above the wire format.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixes the interface both transports implement, and lands the controls that are
out of scope for this release as single-warning stubs so the follow-up work is
purely additive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Digest-authenticated access to the director API, following the X_MYENERGI-asn
header to the account's active server and treating its absence as a credential
failure rather than a transport error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Self-review caught the nested test helper failing 100% interrogate docstring
coverage, which the pre-commit hook set does not check for locally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_resolve_asn had no timeout/status handling of its own, so a network failure
or director outage during the cold-start request escaped as a raw aiohttp
exception, or worse, was misdiagnosed as bad credentials when a non-200 error
page happened to be missing the ASN header. It now applies the same
status/timeout handling as _request, checking the status code before treating
a missing header as a credential failure. Also switches the reason values
passed to record_api_call to the vocabulary predbat_metrics.record_api_call
documents and every other component already uses (auth_error,
connection_error, server_error, client_error), replacing this task's
non-standard unauthorised/timeout/http_<status> strings so failures aggregate
correctly by reason. Adds direct-transport coverage for 401, non-200,
timeout, ASN migration mid-session, and the missing-header-vs-401 precedence
rule, plus the two new _resolve_asn failure paths.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bearer-token access to api.s18.myenergi.net with a cached device list and
per-device-class boost bodies, so a Zappi never receives durationMinutes and an
Eddi never receives mode or parameters.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…on label

Wraps the cloud transport's JSON decode in the same try/except pattern as the
rest of _request, so an undecodable 200 body (an HTML error page slipping
through content_type=None) or a decoded-but-non-dict device list surfaces as
MyEnergiApiError with reason="decode_error" instead of a raw ValueError or
AttributeError reaching Task 5's MyEnergiError-only catch. Moves the
success record below the decode so a failed decode is no longer counted as a
successful call.

Also fixes MyEnergiDirectTransport._request's aiohttp.ClientError handler,
which still recorded reason="client_error" after Task 3's review settled on
"connection_error" for this case everywhere else (_resolve_asn and both
cloud-transport branches already used it).

Strengthens test_cloud_sets_bearer_header to prove the token is re-read on
every request, not just captured once, and adds direct assertions on the
record_api_call reason label for every failure branch in both transports.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MyEnergiDirectTransport._request had the same undecodable-body hole the cloud
transport's decode fix closed last round: response.json(content_type=None)
disables aiohttp's content-type guard, so a captive portal or misconfigured
proxy in front of the resolved ASN host could return a 200 with a body that
raises json.JSONDecodeError, escaping as a raw ValueError instead of
MyEnergiApiError. The direct transport is the default path for self-hosted
users and Task 5's run loop depends on only MyEnergiError ever leaving a
transport, so this could not stay deferred.

_resolve_asn never decodes a body (it only reads the ASN response header), so
it needed no change. fetch_devices already guards the decoded shape
(isinstance(payload, list) / isinstance(group, dict)), so no redundant shape
guard was added here - only the decode path was open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Selects a transport from the configured credentials, polls on the component base
cadence and publishes per-device entities. A failed poll returns False without
republishing, so the last good reading survives a transient outage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ponent test

initialize() has two distinct "no transport" branches - direct and oauth - and
only the direct one had an assertion. Add the oauth case so both paths that
leave self.transport None are actually exercised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_init_oauth() overwrites self.auth_method with its own "oauth"/"api_key"
vocabulary, so the direct branch was logging "starting with the api_key
transport" regardless of what the user configured. Keep the user-facing
value in its own auth_method_config attribute instead.

check_and_refresh_oauth_token()'s return value was discarded, unlike every
other OAuth component in the codebase. When a token needs re-authorisation
(oauth_failed), the poll now stops instead of hitting the API every cycle
with a dead token and never surfacing the problem.

Also: assert the boost switch's "on" state and the charging binary sensor's
value (previously only ever exercised against an "off"/falsy fixture), cover
fetch_devices() returning an empty list (the warn branch and the "must not
wipe existing devices" behaviour), and share one STATUS_CHARGING constant
across the state tables and publish_data() instead of repeating the literal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the COMPONENT_LIST entry with required_or on the two credential sets so the
component only starts when one transport is fully configured, plus the matching
APPS_SCHEMA keys.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…check, required_or comment

The registered display name was the only non-descriptive one in COMPONENT_LIST and
was leaking into user-facing "Initialising ..." log lines as bare "myenergi" -
changed to "myenergi Zappi/Eddi" to match sibling entries. The registration test
only checked that every declared arg matches an initialize() parameter, not the
reverse, so a parameter added to initialize() without a matching args entry would
pass silently; added the missing direction. Documented why required_or gates on
api_key/key, matching the precedent set by deye and solis.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Zappi session energy sensors are set as a list on car_charging_energy so several
chargers sum, and the first Eddi feeds iboost_energy_today. Uses set_arg_auto so
an explicit apps.yaml value is reported rather than silently replaced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…set_arg_auto

The two-Zappi/two-Eddi test now inserts devices out of serial order so it can
only pass if automatic_config() genuinely sorts by serial - the previous
version happened to match dict insertion order and would not have caught a
missing sort. automatic_config() itself now checks device.kind == DEVICE_KIND_EDDI
before picking "the first Eddi", instead of treating any non-Zappi as one, so a
future third device kind cannot be silently wired into iboost_energy_today.
Added tests for an Eddi-only site, for set_arg_auto's apps.yaml-override
reporting (previously indistinguishable from plain set_arg under MockBase),
and strengthened the disabled/runs-once tests to assert the poll actually
reached the wiring code rather than only that nothing changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Boost switches with a companion number entity for the amount, queued onto the run
loop so API calls never run on the event thread. A Zappi outside Eco or Eco+ is
refused locally rather than issuing a call myenergi would reject.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Standalone CLI for exercising either transport against the live API, plus the
components and apps.yaml documentation, including the iboost_today limitation
that follows from the session-scoped energy sensors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… gate

The CLI printed OAuthMixin's internal auth_method vocabulary ("api_key") instead
of the user-facing auth_method_config ("direct") that run() already uses, and
ignored --token-hash on its own when picking a transport. The docs described
poll_seconds as rounding up rather than to the nearest multiple of 60, implied
token_hash alone is a usable OAuth config despite the required_or activation
gate, implied car_charging_hold defaults off, and overstated how often the
not-implemented-control warnings repeat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…arging

The myenergi component publishes a Zappi's plug status straight from the
pilot state, and pilot states C1 and D1 normalise to "EV ready to charge" -
a car that is plugged in and waiting for its slot. None of the apps.yaml
templates carried that value in car_charging_planned_response, so Predbat
read a plugged-in, ready car as not planned to charge and dropped it from
the plan. Added it to every template that lists the myenergi-aware
responses, commented examples included.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…h retry

Ten issues found by the whole-branch review, all in myenergi.py:

The cloud device-list cache never aged. meta_age_seconds was set to its
maximum in __init__ and to zero on refresh, and nothing in between ever
increased it, so GET /devices ran exactly once per process and a device
added, removed or renamed in the myenergi app stayed invisible until
Predbat restarted. It is now a wall-clock stamp compared against
CLOUD_DEVICE_LIST_MAX_AGE.

The direct transport checked for the X_MYENERGI-asn header before the HTTP
status, so a 503 from the account server surfaced as "check the hub serial
and API key" and sent a self-hosted user off to regenerate a perfectly good
key during a myenergi outage. The status checks now run first, matching
_resolve_asn, whose docstring already explained why.

Auto-configuration left car_charging_planned broken. Every apps.yaml
template ships a regex for that key targeting the third-party ha-myenergi
integration's entity names, which do not match the ones this component
publishes, so Predbat logged "failed to match - disabling this item" and
fell back to the car_charging_threshold heuristic. automatic_config now
wires the Zappi plug status sensors into it alongside car_charging_energy.

The /cgi-* command endpoints answer HTTP 200 whether or not they acted,
carrying the outcome in a {"status": N} body that send_boost and
cancel_boost discarded. A refused boost - an Eddi already at maximum tank
temperature has no pre-check at all - logged as a success and the switch
quietly flipped back on the next poll. A non-zero numeric status now
raises, and switch_event_handler returns the result rather than dropping
it, so a refusal reaches the run loop's "control failed" warning.

A 401 mid-poll raised MyEnergiAuthError and simply ended the cycle. The
proactive check_and_refresh_oauth_token only covers a token that has
reached its stated expiry, so a token revoked before then wedged the
component until restart. It now calls handle_oauth_401 and retries the poll
once, as fox.py, deye.py and solis.py do.

Also: a cloud device whose status call fails no longer costs the whole
poll; poll_seconds is capped at 30 minutes and the success timestamp is
stamped only by a cycle that actually polled, so the health check reflects
real API contact without a slow poll reading as a failure; the entity
lookup anchors on a whole prefix; number_event_handler is guarded on the
boost suffixes symmetrically with switch_event_handler; boost amounts round
rather than truncate and a target time is zero padded to HHMM; an
unsupported device kind is refused rather than sent an Eddi command; the
CLI connects before polling; and an aiohttp too old for DigestAuthMiddleware
is reported with an actionable message instead of a bare AttributeError.

Tests cover every one of these, including the three production behaviours
that previously had no coverage at all - the poll_seconds gate, the cloud
cache-staleness branch and the Eddi kind guard - plus a reason-label table
for the direct transport matching the cloud transport's. The two "unknown
entity" tests now load a known device first, so their lookup loop actually
runs. All fourteen fixes were mutation-checked: reverting any one of them
fails the suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…vers

The documented "known limitation" was empirically false. It claimed
iboost_today is the midnight-to-now difference and so under-reports after a
mid-day Eddi reset, and that car_charging_energy is unaffected. In fact
fetch.py reads a series that has already been through
minute_data_load(..., clean_increment=True) and clean_incrementing_reverse,
which rebases the counter on a reset: a realistic Eddi day of 3.0 kWh
yesterday and two sessions of 2.0 and 1.5 today, with a mid-day reset, runs
through Predbat's own minute_data to exactly the right 3.5 kWh.

The real limitation is narrower and was undocumented. A drop only counts as
a reset once a sample reads zero or the fall exceeds 1 kWh, so a session
ending below roughly 1 kWh without a zero sample is not rebased and its
energy is lost - two 0.6 kWh sessions total 0.600 against a truth of 1.20.
That loss is in the shared cumulative series, so it hits car_charging_energy
exactly as hard as iboost_today, the opposite of what was documented.
Corrected in docs/components.md, in the automatic_config docstring, and in
the design record.

Also corrected: auto-config now covers car_charging_planned, so the docs no
longer claim more or less than it does; the "whichever matches your
auth_method" gate does not exist, components.py's required_or is an
unconditional either/or; the not-implemented controls are a reserved
interface nothing can reach rather than something a user can attempt; the
Eco/Eco+ pre-check is one specific check, not a general guarantee that no
rejected call is ever made; the Eddi sensor's more consequential effect is
the load subtraction gated on iboost_energy_subtract, which runs whether or
not iboost is enabled; and auto-config latching until restart is now stated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 23, 2026 15:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The myenergi component activation gate currently prevents valid OAuth refresh-only configurations (token_hash-only) from ever constructing the component, and the cloud transport’s boost path should reject unsupported device kinds rather than treating everything as Eddi.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a new pluggable myenergi component to Predbat to monitor Zappi (EV charger) and Eddi (hot water diverter) devices, auto-wire their energy/plug-status sensors into Predbat’s optimiser inputs, and expose boost controls—supporting both the direct digest API and the official cloud OAuth API behind a shared transport interface.

Changes:

  • Introduces apps/predbat/myenergi.py implementing device normalisation, two transports (direct + cloud), publishing, auto-config, boost controls, and a CLI harness.
  • Registers the new component + config keys, and adds comprehensive unit tests wired into the existing test runner.
  • Updates shipped templates to accept the additional Zappi plug state ev ready to charge, plus documentation and spellcheck dictionary updates.
File summaries
File Description
templates/teslemetry.yaml Adds ev ready to charge to car-plug planned-response values (commented template list).
templates/tesla_powerwall.yaml Adds ev ready to charge to car-plug planned-response values (commented template list).
templates/sunsynk.yaml Adds ev ready to charge to car-plug planned-response values (commented template list).
templates/solis_cloud.yaml Adds ev ready to charge to car-plug planned-response values (active list).
templates/solax_sx4.yaml Adds ev ready to charge to car-plug planned-response values (commented template list).
templates/solax_cloud.yaml Adds ev ready to charge to car-plug planned-response values (active list).
templates/solaredge.yaml Adds ev ready to charge to car-plug planned-response values (active list).
templates/solar_assistant_growatt_sph.yaml Adds ev ready to charge to car-plug planned-response values (commented template list).
templates/solar_assistant_growatt_spa.yaml Adds ev ready to charge to car-plug planned-response values (commented template list).
templates/sofar.yaml Adds ev ready to charge to car-plug planned-response values (active list).
templates/sofar_modbus.yaml Adds ev ready to charge to car-plug planned-response values (active list).
templates/sigenergy_sigenstor.yaml Adds ev ready to charge to car-plug planned-response values (commented template list).
templates/sigenergy_cloud.yaml Adds ev ready to charge to car-plug planned-response values (active list).
templates/luxpower.yaml Adds ev ready to charge to car-plug planned-response values (commented template list).
templates/hanchu_cloud.yaml Adds ev ready to charge to car-plug planned-response values (active list).
templates/givenergy_givtcp.yaml Adds ev ready to charge to car-plug planned-response values (active list).
templates/givenergy_ems.yaml Adds ev ready to charge to car-plug planned-response values (active list).
templates/givenergy_cloud.yaml Adds ev ready to charge to car-plug planned-response values (active list).
templates/ginlong_solis.yaml Adds ev ready to charge to car-plug planned-response values (commented template list).
templates/ge_cloud_octopus_standalone.yaml Adds ev ready to charge to car-plug planned-response values (active list).
templates/fronius.yaml Adds ev ready to charge to car-plug planned-response values (commented template list).
templates/fox_cloud.yaml Adds ev ready to charge to car-plug planned-response values (active list).
templates/ep_cube_cloud.yaml Adds ev ready to charge to car-plug planned-response values (active list).
templates/enphase_cloud.yaml Adds ev ready to charge to car-plug planned-response values (active list).
requirements.txt Pins aiohttp>=3.12 to support digest middleware usage.
docs/superpowers/specs/2026-08-23-myenergi-integration-design.md Adds a detailed design spec for the integration.
docs/components.md Documents the new component, options, entities, controls, and limitations.
docs/apps-yaml.md Documents new myenergi_* configuration keys and behaviour.
apps/predbat/unit_test.py Registers the new myenergi test suite in the test runner.
apps/predbat/tests/test_myenergi.py Adds a comprehensive test suite for normalisation, transports, publishing, auto-config, controls, and template acceptance.
apps/predbat/myenergi.py Implements the myenergi component, transports, publishing, auto-config, controls, and CLI harness.
apps/predbat/config.py Adds new config schema keys for the myenergi component.
apps/predbat/components.py Registers the myenergi component in COMPONENT_LIST.
.cspell/custom-dictionary-workspace.txt Adds new domain words used by the integration and docs.
Review details
  • Files reviewed: 33/35 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/predbat/components.py Outdated
# transport's local hub credential, key is the cloud transport's access token.
# Without this the component would start for every instance since all
# individual args are optional to allow either auth mode.
"required_or": ["api_key", "key"],
Comment thread apps/predbat/tests/test_myenergi.py Outdated
Comment thread apps/predbat/myenergi.py
Comment on lines +654 to +661
if device.kind == DEVICE_KIND_ZAPPI:
body = {"mode": "normal", "parameters": {"energy": _boost_units(amount)}}
if target_time:
body = {"mode": "smart", "parameters": {"energy": _boost_units(amount), "targetTime": target_time}}
else:
body = {"durationMinutes": _boost_units(amount)}
await self._request("POST", "/devices/{}/boost".format(device.device_id), body=body)
return True
springfall2008 and others added 5 commits August 23, 2026 17:00
Include token_hash in the myenergi activation gate. A refresh-only OAuth setup
carries no access token, and initialize() already accepts that, so gating on key
alone meant such a config never constructed the component at all.

Reject unsupported device kinds in the cloud transport's boost and cancel paths
rather than defaulting them to the Eddi body, matching the direct transport.

Add 'ev ready to charge' to alphaess_cloud.yaml, which landed on main after this
branch and carries the same car_charging_planned_response vocabulary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_make_web() stubs my_predbat.components with a SimpleNamespace, but my_predbat is
shared across the whole run, so the stub outlived the test and broke every later
test calling a real method on it - is_running() raised AttributeError on
components.is_all_alive() in test_web_functions. Save and restore it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The per-device tolerance added for a single unreachable device also swallowed the
case where every device failed: fetch_devices returned an empty list, run() read
that as an empty site, kept the previous readings and stamped success. A site
whose every device was erroring therefore reported healthy indefinitely.

Raise instead when the transport knows about devices and reads none of them, so
run() returns False, the error count rises and components.py's 60 minute staleness
check eventually marks the component unhealthy. An account with genuinely no Zappi
or Eddi still reports no devices rather than an error, and a partial failure still
keeps the healthy devices visible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The docs, docstring and spec all said a session ending below roughly 1 kWh is
under-counted only when no reading lands on zero, which implied a zero reading
would rescue it. It does not: minute_data() smooths any fall under 1 kWh as a dip
in the data (utils.py:565) before clean_incrementing_reverse() (utils.py:740) ever
looks for a reset, so the zero is gone by then. State the behaviour as it is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@springfall2008
springfall2008 merged commit 8346c43 into main Aug 23, 2026
2 checks passed
@springfall2008
springfall2008 deleted the feat/myenergi-component branch August 23, 2026 17:43
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