Conversation
Setup failed for every user with "invalid_token". The config flow
validated the access token against the hosted-UI endpoint
/oauth2/userInfo, which requires the "openid" scope. The Daze web
portal issues access tokens scoped "aws.cognito.signin.user.admin"
and never includes "openid", so Cognito rejected every token a user
could obtain:
HTTP 401
Access token does not contain the 'openid' scope
The tokens themselves were valid. Refreshing produced another token
with the same scope set, so there was no user-side workaround.
Replace both userInfo call sites with the Cognito user pool GetUser
operation, which accepts that scope and returns the same profile
attributes, including the email address the config flow needs to look
up networks.
Three behavioural differences from userInfo are handled explicitly:
- The access token travels in the request body, not in an
Authorization header.
- Cognito responds with application/x-amz-json-1.1, so the JSON
decode must relax the content type check or aiohttp raises
ContentTypeError.
- An invalid token yields HTTP 400 with NotAuthorizedException rather
than HTTP 401. The retry-on-401 path in DazeApiClient._request would
therefore never fire, so async_get_user_info performs its own
refresh-and-retry.
Failed validation now logs the error type rather than the full
response body.
Add tests/test_auth_getuser.py, which imports the real modules instead
of re-implementing their logic, covering request shape, scope
acceptance, attribute flattening, the refresh-and-retry path, and a
guard against reintroducing the userInfo endpoint.
Add tools/check_daze_tokens.py, a standalone diagnostic that reproduces
each authentication step against Cognito and reports which one fails.
It reads tokens from hidden prompts and never stores, logs, or echoes
them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Marks the release containing the Cognito GetUser authentication fix, which repairs config flow setup for all users. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The git remote already pointed here, but the integration manifest and README still referenced the upstream repository. Users installing from this fork would have filed issues on the upstream tracker, and the README's HACS install URL would have sent them to upstream instead. Repoint codeowners, documentation and issue_tracker in the manifest, and the badges and install URL in the README. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
For some accounts GET /v3/networks/{uid}/rechargeSessions returns HTTP
404 with an empty body. The coordinator treated that as a transient
error, so it re-requested on every poll and logged a warning twice per
attempt, once from the request layer and once from the caller.
Probing confirmed the condition is durable rather than transient. With
the same token and network UID, /networks/{uid}/evses and
/users/{email}/networks both return 200, while twelve plausible
spellings of the session path all return 404 with an empty body,
including paths that do not exist. This API answers 404 for any
unrouted path, so the session resource is simply not reachable for the
account.
Add ApiNotFoundError, a subclass of ApiError so existing handlers are
unaffected, and raise it for 404 without logging the empty body.
In the coordinator, treat a missing session endpoint as durable: log
once at info, explain that live metrics and charge control still work,
and retry hourly instead of every 30 seconds.
Also throttle the normal session fetch to once every five minutes and
cache the result between polls. Session history only changes when a
charge ends, so requesting up to 1000 records twice a minute was
needless load and a rate-limiting risk.
Session and lifetime sensors stay empty on affected accounts. The
remaining 15 sensors, charge control, current limiting and eco mode
are unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers the recharge-session resilience work in 1fe980c, which shipped after the 0.1.1 bump and changed runtime behaviour: - HTTP 404 from the session endpoint is now a durable condition, logged once at info instead of a warning pair every 30 seconds - Session history is fetched at most every five minutes and cached between polls, instead of pulling up to 1000 records twice a minute Also make tools/probe_sessions_endpoint.py discover the network UID and wallbox serial from the API, the same way the config flow does, so the only thing it asks for is the refresh token. It reports every network and charger it finds, and notes when an account has more than one charger, since the config flow only ever uses the first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every entity was created but read Unknown, with no error logged. The
API answers 200, so nothing failed; the value functions simply looked
in the wrong place and .get() returned None for all 16 fields.
Captured payloads from a live DT01 show the data spread across two
endpoints and three nesting levels:
- Live session metrics (power, energy, currents, voltages) arrive under
chargeSession in the remoteInfo response, not at the top level. The
field names were always correct, only the depth was wrong.
- Temperatures, the grid limit, eco mode, the configured current and
the photovoltaic flag appear only in the EVSE record from
/networks/{uid}/evses, which the coordinator never fetched.
- Status is reported as the integer evseState plus independent pause
and error flags. Nothing returns the string evseStatus that the
sensor catalog and the charge switch compared against.
Add payload.py to flatten both responses into one mapping, with
precedence ordered so the live session wins over the cached socket
snapshot. It imports nothing from Home Assistant so it is directly
testable.
Derive evseStatus from evseState, the pause flags, the system error and
the active flag. Only state 3 is confirmed: it was observed while the
charger delivered 2688 W with a session running. Unrecognised values
report idle rather than guessing, so nothing is falsely shown as
charging.
Fetch the EVSE record in the coordinator, cached for two minutes since
it holds configuration and slow-moving readings.
Repoint the four catalog fields whose names genuinely differ:
boardTemperature to lastBoardL1Temperature, caseTemperature to
lastCaseTemperature, gridMaxPower to supplyGridMaxPower, and the
photovoltaic flag. Add nextScheduleInfo to the schedule key list.
Add tests/test_payload.py, whose fixtures are captured API responses
with device identifiers replaced. It asserts that no live sensor reads
None, that the catalog never reads a field the API does not return, and
that unknown states are not reported as charging.
Ignore diagnostic output files by name so captured device data cannot
be committed.
Bump version to 0.1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a version badge as the first item in the badge row, reading the latest semver tag from the repository. It tracks the tags rather than hardcoding a number, so publishing a release updates it with no README edit and it cannot drift from the manifest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Starting or stopping a charge failed with HTTP 422:
ErrorWrongSessionID: Failed to suspend session: Wrong Session ID.
Error code: 4121
playcharge resumes an existing session rather than starting one from
nothing, and the body must name both the charger and the session. The
integration posted an empty body.
Three behaviours were measured against hardware, in this order:
- empty body: HTTP 422, ErrorWrongSessionID
- {"sessionId": ...}: HTTP 200 with an empty error list, and the
charger stays paused. Accepted is not resumed.
- {"evseSerialNumber": ..., "sessionId": ...}: HTTP 200, and within
ten seconds evseState moves 6 to 5, isPaused clears and
evseSuspensionReason returns to 0.
Restoring maxExternalChargingCurrent was also tried, since a paused
session reports a zero current limit. It returned 200 and changed
nothing across 30 seconds, so the zero limit is a symptom of the pause
rather than its cause.
Send both fields from switch.py and from the three services, reading
the session ID out of the coordinator's merged payload. Keep sending
the serial when no session is known, so the body is never empty.
stopcharge sends the same shape. That symmetry is assumed, not
measured: only the resume direction was verified.
Record two more confirmed evseState values: 5 is connected but drawing
no power, reported as idle rather than charging so the switch does not
read on while the car takes nothing, and 6 is paused.
Bump version to 0.1.4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every command failure showed the same notification, "Failed to start charging. Check that the car is connected and try again", regardless of cause. That was actively misleading: it appeared when the car was connected and the real problem was that the charger had no paused session to resume. Add ApiCommandRejectedError, carrying the upstream error code, and map the two codes observed against hardware: - 4121, sent with no session ID or naming a session that is not paused: there is no paused charging session to resume - 101, returned from a connected but idle charger: the charger could not carry out the command, which usually means it is not in a state where that command applies The charge switch now relays that explanation instead of the stock line, and logs it at info rather than warning, since a refusal is not a fault. Make tools/try_resume.py state-aware. It previously refused to run without an open session, which is exactly the state that still fails. It now picks resume variants when the charger is paused, leading with the confirmed working call, and start variants otherwise. Starting from a connected but idle charger is still unsolved. Resuming a paused session works and is unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Start and stop failed with HTTP 500 and error code 101, "Server error while requesting rpc server side". This is not a rejection: the Daze service relays commands to the wallbox over its own RPC link, and that link fails intermittently. The same command succeeds on a later attempt, which is why the vendor app appears to need several presses. Around six attempts were observed before a command took effect. The integration sent the command once and reported an error, so a working setup looked broken. Retry the command up to eight times, 1.5s apart, on code 101 or any 5xx without a structured code. A wrong-session rejection (4121) is deterministic and is still surfaced immediately. The worst case is about fifteen seconds, which is tolerable for an operation that physically switches a charger. Log the attempt number when a command succeeds after a retry, so the budget can be tuned from real use rather than guessed again. Correct the earlier reading of code 101. It was mapped as "the charger is not in a state where that command applies", which was wrong and produced a confident, misleading message. Carry the HTTP status on ApiError so retry decisions do not depend on parsing the message text. Add a retry mode to tools/try_resume.py that sends one known-correct command repeatedly and reports which attempt produced an observed state change, for start and for stop. This is how the six-attempt figure was measured. Fix the test fake, which returned Python repr rather than JSON from text() and so hid the error-code parsing from the tests. Bump version to 0.1.5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two problems showed up once commands started working. A start does not go straight to charging. The charger passes through evseState 5 first: the session is authorised and unpaused but the car is not yet drawing. That state was reported as idle, which is wrong in both directions. It hid a real state from the status sensor, and it made the charge switch read off immediately after a successful start, so the toggle appeared to snap back as though the command had failed. Add waiting_for_ev as a status and as a declared option on the status sensor, with English and Italian labels. The charge switch now reads on for charging and for waiting-for-EV, since the user's intent has been carried out in both cases, and only reads off when idle or paused. Separately, state changes are not instant. Pausing in particular takes several seconds to register. The coordinator refreshed once immediately after a command, which reads the state before the change and leaves the entities stale until the next ordinary poll thirty seconds later. Schedule further reads at 3, 8, 15 and 30 seconds after any command, so the entities follow the transition. They are scheduled rather than awaited, so a service call still returns promptly. Applied to the switch, the current limit, the operation mode and the three services. Bump version to 0.1.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README pointed entirely at this fork after the metadata was repointed, leaving no reference to where the integration came from. That was wrong: the architecture, config flow, entity model, sensor catalog and API client are Andrea Restello's work, including the reverse engineering of the undocumented Daze web API. Add a fork notice near the top of the README and a Credits section listing what this fork changed and what it did not. Add a NOTICE file stating the same for anyone reading the source rather than the README. Spell the name out in the LICENSE copyright line, which read "andrea". The terms and the year are unchanged. Nothing was added to manifest.json: hassfest validates it against a strict schema and rejects unknown keys, which would break CI. The upstream repository is registered as a git remote instead, which is local configuration rather than a tracked file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A command that needed six attempts left six warnings in the log, so a
working retry looked like a recurring error:
API error (HTTP 500) on POST .../commands/playcharge:
{"code":101,"message":"Server error while requesting rpc server side"}
The request layer logged every failure at warning before raising, and
the retry wrapper then handled it silently. The user saw the noise and
not the recovery.
Give _request a log_errors switch. The retry wrapper sets it false, so
attempts it intends to handle are logged at debug. If every attempt
fails, the wrapper logs exactly one warning naming the command, the
attempt count and the elapsed time.
Add tests asserting that a command which succeeds after retries emits
no warnings, and that exhausting the budget emits exactly one.
Bump version to 0.1.7.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eight retries 1.5s apart all failed. The whole burst finished inside eleven seconds, so the command was effectively tried once against an outage that had not cleared. Manual presses that did eventually work were roughly sixteen seconds apart. That points at the RPC link needing time to recover rather than simply more attempts, so the delay now grows with each try: 1.5, 3, 4.5, then 6s for the rest, spreading eight attempts across about 33 seconds. This is a long time to hold a service call. It is still shorter than pressing the button by hand until it takes, which is what the vendor app requires. Reword the failure message to say how long it tried and to place the fault where it belongs. The previous text sent the user looking at the car and the charger, neither of which is involved. Bump version to 0.1.8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Testing a start or a stop meant answering six prompts. Add flags so a
run is a single command:
--direction start|stop which command to send
--attempts N how many times to try
--gap S seconds between attempts
--yes skip the per-attempt confirmation
The refresh token stays on the hidden prompt and is deliberately not
accepted as a flag: it would end up in the shell history and in the
process list.
Raise the interactive default gap from 2s to 6s, matching the backoff
the integration now uses.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The command tools succeed on the first attempt where the integration fails, and session ID freshness is one concrete difference between them. Session IDs change whenever one session ends and another begins. Three different IDs appeared over a single evening on the same charger. The tools read the ID from the charger immediately before sending, so it is always current. The integration read it from the coordinator, whose copy can be a full poll interval old and may name a session that has already closed. Read it fresh inside the command instead. The explicit parameter remains as an override for tests and the tools. Costs one extra GET per command press, which is not measurable next to a command that can take half a minute to land. Bump version to 0.1.9. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Commands now read the session ID from the charger when they are not given one, so tests that pass None need a response queued for that read. Two were missed, and 1513ab2 was pushed with them failing. One used a literal response dict rather than the shared constant, so the edit that updated the others did not match it, and the fixture it referenced was never defined. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Credits section described the fork's changes without saying who made them, and the list was written several fixes ago. Attribute the fork's changes to Pedro Tarrinho, both in the notice at the top of the README and in a dedicated section, and note that the upstream work remains Andrea Restello's. Rewrite the change list to cover everything the fork now carries, grouped by area: setup, reading data, charge control, and robustness and diagnostics. Each entry says what was wrong as well as what changed, since several of these were only findable by measuring the API's real responses. Also correct the EVSE status row in the sensor table, which still listed the original four states and omitted waiting_for_ev and offline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The charge switch flipped back to its old state for several seconds after being toggled. The command succeeded, but the coordinator refreshed immediately afterwards and the Daze cloud still reported the previous state, so the entity was corrected to the wrong value and only recovered on a later poll. Report the commanded value straight away instead. After a successful command the switch sets its assumed state, writes it, and schedules a single coordinator refresh ten seconds later rather than refreshing at once. Observed transitions completed in nine to twelve seconds. The guess is bounded in both directions. It is dropped as soon as the charger agrees, so real changes are not delayed, and abandoned after twenty seconds, so a command that silently failed cannot leave the UI asserting something untrue. assumed_state is reported while the guess is in force, so the frontend can show it as unconfirmed. The decision itself lives in payload.resolve_optimistic, which imports nothing from Home Assistant and is covered by tests for each case: no guess, guess versus stale reading, agreement, expiry, and an unknown reading. Separately, make the poll interval configurable through the integration options, bounded between 10 and 600 seconds. Options take precedence over the value captured at setup, and the entry already reloads when options change, so a new interval applies immediately. English and Italian labels included. The number and select platforms keep the existing settle refreshes: they have no boolean state to guess at, so there is nothing to be optimistic about. Bump version to 0.2.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Setting the current failed with HTTP 422:
code 369, MaxExternalChargingCurrentOutOfRange
The number entity advertised a fixed 6 to 32 A range, but 32 A is the
installation rating, not what the charger will take. A grid power cap
puts the real ceiling well below it. On the charger this was found
with, a single-phase unit behind a 3000 W supplyGridMaxPower reports
lastMaxInstallationCurrent 32000 and sccLimit 11739, and rejects
anything above 11739: 11739 mA at 230 V is 2700 W, ninety percent of
the cap, while 32000 mA would be 7360 W.
Read the ceiling from the charger instead, preferring sccLimit and
falling back to the installation rating, then to 32 A before the first
poll. The result never drops below the 6 A industry minimum, so a
nonsensical reported limit cannot make the entity unusable.
Map error 369. Despite naming the RPC server it is a validation
failure, so it is surfaced immediately rather than retried, and the
notification now states the highest value the charger currently
accepts.
Route the two configuration writes through the same wrapper as the
charge commands, so setting the current or the eco mode also retries
the intermittent RPC failure instead of failing on first contact.
Set the version to 0.1.10, continuing the 0.1.x line rather than the
0.2.0 tagged in the previous commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0.1.10 capped the charging current at sccLimit, on the reasoning that a 3000 W grid cap put the real ceiling near 11739 mA. That was wrong and the cap was harmful. The charger is rated 1.5 to 7.4 kW single phase, which is 6.5 to 32 A, so 32 A is genuinely within its capability. And sccLimit is not an independent ceiling: on the live charger it read 11739, identical to both maxExternalChargingCurrentInMilliAmps and lastMaxChargingCurrent, which are the current setting. Capping the slider at it would have pinned the slider to wherever it already sat, making the control useless. Use lastMaxInstallationCurrent alone, which is the installation rating and bounds the hardware: 32000 for this charger, and correctly lower for a 16 A installation. Better than the hardcoded 32 A it replaced, without inventing a limit. What actually causes MaxExternalChargingCurrentOutOfRange is still unknown. The field that would explain it is not in the payload, so it cannot be applied in advance. The error mapping added in 0.1.10 stays: the failure is reported immediately rather than retried. Add tools/probe_current_range.py to measure the accepted range by walking a ladder of values and restoring the original setting afterwards, including on interrupt. Bump version to 0.1.11. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A read timeout on the second ladder step crashed the script. That is the worst place for it: the loop writes configuration to the charger, so a crash can leave it on a probe value rather than the user's own. The restore did run, via the finally block, but only by luck of where the exception landed. Retry a stalled request twice before giving up, and treat a timeout as a result rather than an exception. Correct the ladder. It started at 6000 mA on the assumption that 6 A is the floor, and 6000 was rejected as out of range. The charger's setting at the time was 6521 mA, which is exactly 1500 W at 230 V and matches its 1.5 kW rating, so the limits look power based rather than current based. The ladder now brackets that floor and reaches the 7.4 kW rating at the top. Report the implied wattage beside each result, using the charger's own voltage reading, so a power-based limit is visible directly. Accept --values to override the ladder. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bottom of the charging current slider always failed with
MaxExternalChargingCurrentOutOfRange. The entity offered 6 A, the EVSE
industry minimum, but the charger enforces a minimum power instead.
Measured by walking a ladder against the charger at 232 V:
6000 mA = 1392 W rejected
6400 mA = 1485 W rejected
6521 mA = 1513 W accepted
32000 mA = 7424 W accepted
The boundary is 1500 W, matching the unit's 1.5 kW rating, so the
minimum current depends on the supply voltage and cannot be a
constant. At 232 V it is 6466 mA, which is why 6 A was always short.
Compute the floor from the power minimum and the measured voltage on
L1, rounded up to a selectable 0.1 A step, and never below 6 A since
no EVSE charges under that. Three-phase arithmetic yields a current
below 6 A, so the 6 A floor applies there instead.
The ceiling needed no change: 32000 mA was accepted at 7424 W, within
the unit's 7.4 kW rating and equal to the reported installation
rating.
sccLimit is confirmed as a dead end. Across three reads it was 11739,
6000 and 6521 while the setting was 11739, 6521 and 6521: it lags the
setting rather than bounding it.
Tests reproduce the measured boundary directly, asserting that every
rejected value falls below the computed floor and every accepted one
does not.
Bump version to 0.1.12.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two kinds of check, for two kinds of failure. tests/test_qa_invariants.py sweeps supply voltages from 207 to 253 V and installation ratings from 10 to 32 A, asserting the properties that must hold for any charger rather than the one that was measured: the floor never exceeds the ceiling, every offered value clears the 1500 W power minimum, rounding up costs at most one step, both bounds land on selectable steps, the floor falls as voltage rises, three phase falls back to the 6 A EVSE minimum, and degenerate payloads still produce usable bounds. It also asserts that every reachable status is a declared sensor option and that the switch never reports on while the status says otherwise. tools/qa_verify_current.py checks the thing no offline test can: that the charger's draw actually follows the limit. HTTP 200 has already proved misleading twice in this integration, once for playcharge and once for the current change, so this lowers the limit below the present draw, watches the measured current for up to 90 seconds, then raises it and watches again, before restoring the original. It requires an active charge, since a limit change cannot be observed against a zero draw, and it says plainly that lowering is the meaningful direction because a car at its own ceiling will ignore extra headroom. tests/run_all.py runs the three standalone suites and reports a combined total, so a check before deploying is one command. No integration behaviour changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Setting the charging current failed twice in a row with the same message: the Daze service could not reach the wallbox after 8 attempts over 33 seconds. Holding the call open longer is not the answer. The outage lasts minutes, and 33 seconds of blocking is already more than a user should wait to move a slider. Split the two cases the API conflates. A refusal is final and is still reported at once: a current outside the accepted range, or a session that cannot be resumed. An unreachable RPC link is not final, and the same command usually lands a minute or two later. Commands now make three quick attempts inline, about four seconds, and on an unreachable link hand off to the coordinator, which retries at 15, 30, 60, 120 and 240 seconds. That covers roughly seven and a half minutes without the user waiting on any of it. Success refreshes the entities; only exhausting every attempt raises a notification. A second request for the same control supersedes the first, so nudging a slider repeatedly does not stack up retries. Applied to the charge switch, the current limit and the operation mode. The switch also sets its optimistic state while the retries run, so the toggle reflects the intent rather than snapping back. Expose the attempt budget on the API command methods so the caller chooses between the short inline path and the long default. Bump version to 0.1.13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dragging the charging current slider appeared to do nothing: the value snapped back to where it started. Optimistic state was added to the charge switch in 0.2.0 but never to the current limit or the operation mode, so both still read from the last poll. That is wrong in two ways. On success the charger takes several seconds to adopt the new figure, so the next read returns the old one. And since 0.1.13 an unreachable charger hands off to a background retry and returns without an error, which left the entity reverting silently while the retry was still running. Show the requested value immediately for both controls, and keep showing it until the charger reports it, using the same rule as the switch. While a background retry is outstanding the request is genuinely in flight, so the display is held rather than expiring after twenty seconds. If every retry fails, the pending value is dropped and the reason is shown. Generalise resolve_optimistic, which was typed for a boolean. A milliamp figure and a mode string lag in exactly the same way. Tests cover all three value kinds, including zero, which is falsy but is a real reading rather than an absence. Bump version to 0.1.14. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The optimistic display logic lives in the entity classes, which import Home Assistant and had therefore never been executed by any test. That is exactly where the reported bug lived: changing the charging current appeared to do nothing. Replace Home Assistant with the smallest stubs the entities actually use and exercise the real number and select classes. Ten tests, all passing, covering: - a new current is displayed immediately on success, while the coordinator still reports the old one. This is the reported bug. - a new current is displayed while a background retry is running, and no error is raised, since the request is still outstanding - the display returns to the charger's reading once it agrees, so a later external change is not masked - the pending value is dropped and explained if every retry fails - a refusal such as an out-of-range current is reported at once rather than retried for minutes - re-selecting the current value sends nothing - the refresh is scheduled for ten seconds rather than run at once - the bounds come from the charger: 6500 to 32000 at 232 V - the mode selector behaves the same way in both respects Register the suite with run_all.py: 94 tests across four modules. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Running the QA tool with the car unplugged exposed two things. evseState 1 is idle: no chargeSession at all, because the car is not connected or the session has ended. Recorded as confirmed, alongside 3, 5 and 6. It was already treated as idle by the fallback, but by default rather than by evidence. More importantly, with no session there is no voltage reading. The floor then falls back to the nominal 230 V and computes 6600 mA, while the charger was sitting at 6521 mA, a value it had demonstrably accepted at its real 232 V. The slider's minimum would have been above its own current value. Clamp the computed floor so it never exceeds the configured current, while keeping the 6 A absolute minimum. The clamp only ever lowers the floor: a charger set to 16000 mA still gets a 6600 mA minimum rather than having the floor dragged up to its setting. A measured voltage still wins when one is available, so the floor is 6500 mA while charging at 232 V and 6521 mA when idle. Bump version to 0.1.15. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Task 8 collapse test asserted only that surplus and the last decision were set, which a no-op fast path satisfies from the preceding healthy ticks. The Task 9 seeding test asserted only that a start mark existed, which seeding it to the present moment satisfies while leaving the charge unstoppable for ten minutes. Found by scanning the remaining briefs for the shape the Task 4 review caught in shipped code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 4's review added twelve tests to tests/test_solar_controller.py, so the absolute counts written into Tasks 5, 8 and 9 no longer matched. State the expected increase instead, which survives the next round of additions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
N2 - A START proceeded even when the current-set was only queued for
the background retry, since _send_command collapsed "sent" and
"queued" to the same True. The car would then start at whatever limit
it already had and import from the grid until the retry landed -
exactly what pure-solar mode exists to prevent. _send_command now
returns True (sent), None (queued), or False (failed outright), and
START only proceeds past the current-set when it is True.
N3 - Solar's retry key (f"{serial}:solar") did not match number.py's
(f"{serial}:current") or switch.py's (f"{serial}:charge"), so a
manual override did not supersede a queued solar command - it could
land minutes later and silently undo the override. Chose to share the
keys rather than cancel in async_stop(): the coordinator's own
async_retry_in_background already cancels by key before scheduling,
and both manual entities already cancel their own key after a
successful direct send, so sharing gives supersession in both
directions with no new code. Current-setting commands (SET, and the
current half of START) now file under f"{serial}:current"; start/stop
(STOP, and the second half of START) file under f"{serial}:charge".
N1 - test_a_missing_sensor_stops_nothing lost its unit attribute on
override, since StubStates.set replaced the whole state. It was
passing by reaching the new unit guard rather than the value-parse
path it names. StubStates.set now carries the previous attributes
forward when none are given, matching real Home Assistant, and the
call itself now passes the unit explicitly too.
N4 (minor) - An unknown charging status (an absent or partial
payload) was treated the same as a confirmed "not charging", handing
_car_draw_w a false zero that then pollutes the five-minute average.
Only an explicit is_charge_enabled(data) is False now yields zero.
N5 (minor) - No total timeout is configured on the session, so
aiohttp's default eventually raises a bare asyncio.TimeoutError,
outside the API's own exception hierarchy. _send_command now catches
it alongside the three Api* exceptions.
N6 (minor) - A START issues two real API calls (current-set,
start-charge) but counted one attempt, undercounting the hourly
backstop at half the real rate for a start-heavy failure. Each call
now counts on its own.
Every fix was verified by reverting it in isolation, confirming the
named test failed, and restoring - including reproducing the
reviewer's own repro for N1 (parse failure mutated to yield 0.0, with
the unit preserved, now fails as it should).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ccess A car that has finished stops drawing while surplus is still high, so the charger goes idle, the controller sees plenty of surplus and no charge, and starts again. The cycle repeats until sunset. The rate limit would blunt this but is the wrong instrument: it is a backstop against bugs, not a substitute for handling a state the design knows about. The draw check goes through _car_draw_w rather than the raw instantPowerAsWatt field, so an unknown draw (missing or unparseable mid-session) waits rather than being misread as "not drawing" and arming an hour-long back-off on a car that may be charging fine. Folded in: _carry_out now cancels the shared background-retry key after a successful direct send, on every branch (STOP, both of START's calls, SET) — mirroring the pattern already used by select.py, switch.py and number.py. Without it, a stale queued retry from an earlier RPC failure can land after a later, successful command and silently undo it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Seeding the start mark to now - MIN_RUN_SECONDS puts it 600s in the past, already beyond the 300s draw grace. With a single shared attribute the first tick after a restart would evaluate the car's draw immediately, and a charger in waiting_for_ev at 0 W would arm an hour-long back-off on a healthy charge at every boot. Task 5's fix round splits the two clocks; this records which one the seeding is for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round-1 review of Task 5 found the back-off never actually pre-empted the rate limit: decide() has no memory of already having started, so while the charger sat idle with surplus sustained it returned START on every tick, and _started_at was reset on every one of those, so the 300s draw-grace window never elapsed. The rate limit — a backstop against bugs, not a substitute for this — ended up doing the back-off's job at ten times the cost (C1). Separately, the start-charge send accepted a queued (None) result as "started" when only the current-set half required a direct True, so a start that had not reached the charger could still arm an hour-long back-off while its own retry chain was still trying to land (I1). Fixing both by reusing the existing _started_at would have re-created the exact collision Task 9 is about to introduce: it seeds _started_at from a charge already running at boot, and that charge passes through a driver-not-drawing state on nearly every start regardless of who issued it. Splitting the concept into two attributes removes the collision instead of documenting around it: - _started_at keeps its existing meaning, the minimum-run clock decide() reads via seconds_since_start, and stays agnostic to who started the charge (Task 9 will seed it from observed state). - _start_issued_at is new: "we issued a start and are waiting to see whether the car draws." Set only when a start _carry_out itself sent reached the charger (True, not None) and only when not already outstanding, cleared when the car is confirmed drawing, when the back-off arms, and when a stop is carried out (so a stale mark can't anchor the next start's grace clock to the wrong start). _check_ignored_start now reads _start_issued_at exclusively. Also: gate the check to ACTIVE mode (M3). A start only simulated never reached the charger, so a dry run must not arm a real back-off from a start that never happened — harmless today, but Task 9's seeding would otherwise let SIMULATE spend an hour previewing nothing. And: a SET that only got queued for the background retry must not cancel that same retry (I3) — _send_command's None means the retry is the only thing still trying to apply the change. Six tests added, each confirmed to fail against the exact regression it targets before being restored (see task-5-report.md for the log). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Task 5's fix round wrapped _check_ignored_start in a mode check, so the old anchor 'immediately before _check_ignored_start' would put the minimum-run seeding under that gate. A simulate dry run of an already-running charge would then report the minimum run time as unelapsed forever and never preview a stop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…epeat starts Fix round 2 of Task 5's review found three ways _start_issued_at could arm a false hour-long back-off, and one way the controller still spent three starts where the design implies one. Important: - A car unplugged shortly after a solar start satisfied every condition the arming check looked for: session gone, evseStatus idle, is_charge_enabled False, draw 0 W. The car did not ignore the start, it left. _check_ignored_start now takes car_connected (already computed by _build_state) and clears the mark without arming when the car is gone, rather than judging a start against a car that is no longer there. - None of the three places that clear _start_issued_at (on arming, on stop, on confirmed draw) had a test; each is a real, worked-out failure mode (a permanent zero-command back-off, a stale mark anchoring the next start's grace to the wrong start, and arming on a car that already charged fine) and each now has one. Minor, folded in: - The mode setter already resets the above/below threshold timers on any change; it now clears _start_issued_at too, so a detour through OFF or SIMULATE can't leave a start-issued mark to be judged against an already-expired grace on return to ACTIVE. - decide() has no memory of an outstanding start, so it repeats START every tick while the charger sits idle with surplus sustained. _carry_out now declines a START while one is already outstanding and its grace has not elapsed, rather than resending it — a retry of a start already in flight is not a fresh one. This takes the repro trace from three starts (6 API calls) to one start (2 calls) before the back-off arms at the same six minutes. Declining does not touch _start_issued_at either way, so it cannot rebuild the round-1 bug by another route. - test_a_sustained_idle_start_does_not_wait_for_the_rate_limit's assertion is pinned to the exact call trace (one start, two calls) instead of merely "under the rate limit", a bound loose enough that reintroducing repeat starts would still have passed it. Five new tests plus one tightened, each confirmed to fail against the exact regression it targets before being restored (see task-5-report.md for the log). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
An audit of the unexecuted briefs found 5 Critical, 14 Important and 12 Minor defects, all in plan text rather than in shipped code. The largest: - The three-phase guard was built on supplyGrid3F, a field that exists nowhere in the API or the codebase, so the refusal could never fire while both its tests passed by injecting the key. The supply is now declared by the user in the options flow, since nothing in the payload reports it. - The restart seeding ran every tick rather than once, so a queued stop reseeded the clock and reissued STOP every 120s until the hourly backstop tripped. - The collapse fast path had no latch and could spend the hourly budget in minutes, then be refused for the stop that mattered. - The collapse path could not achieve its stated purpose: it advanced the stop clock by one tick out of 700-odd seconds, because the clock only started once the five-minute average admitted the drop. The raw collapse now anchors it. - The documentation task would have pushed main five tasks behind the branch, publishing a release containing the plan and none of the feature. The spec carried the same wrong two-minute claim about the collapse path and is corrected with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 9 had grown to about 670 lines, within sixty of Task 4, which produced three Critical findings under review. Large tasks are where this plan's defects concentrate, and one review gate over that much text is what let them through. Task 9 now covers the supply declaration and the refusals, Task 10 covers surviving a restart, and documentation becomes Task 11. Four comments in the controller and its tests that credited the seeding to Task 9 are repointed to Task 10. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The controller is created with the entry and stopped when it unloads, alongside the coordinator's own timers. It is told the reserve from the entry's options rather than starting at zero, and a reserve-only options change no longer reloads the entry. Any limit change or charge toggle arriving through an entity or a service disarms solar control, because the controller writes through the API client and never through an entity. That makes the rule mechanical rather than a flag that has to be set and cleared correctly. Disarming clears the episode's clocks too, so re-arming hours later is not judged against a start nobody is waiting on. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Round 1 review found no behavioural defects in Task 6, only coverage gaps: __init__.py had no test at all, so the load-bearing indentation in async_unload_entry, _reload_signature excluding the solar reserve, and the three service handlers' disarm calls were unverified. Two of the four _disarm_solar call sites (the power entity, async_turn_off) and both placement decisions (before validation, before the offline check) were likewise unprotected. Adds tests/test_init_entry.py, a new standalone harness that loads the real __init__.py against a stubbed Home Assistant, plus six tests in test_entities.py and one in test_solar_controller.py. Every mutation the review named was applied, confirmed to fail its new test, and reverted before this commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
async_unload_entry's final cleanup line indexed hass.data[DOMAIN]
directly, which raises KeyError when setup failed before
hass.data.setdefault(DOMAIN, {}) ever ran. The existing test for "an
already removed entry" only covered a second unload (DOMAIN present,
this entry's key gone) and not this case, so the gap passed as
covered. Split into two tests, one per scenario the docstring named,
and fixed the lookup to tolerate a missing DOMAIN key without
disturbing the entry_data guard above it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The control is one tri-state select rather than a switch and a dry-run flag, so the meaningless combination cannot be selected. It carries the last decision and its reason as attributes, because an autonomous feature that acts silently cannot be understood after the fact, and it refuses to leave 'off' until both grid sensors are set: availability is a hint to the dashboard, not a gate on a service call. The reserve is persisted to the entry's options. Held only in memory it returned to 0 W on every restart, which hands the house's share to the car without saying so. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
async_setup_entry (the only place a persisted reserve is turned back into anything actionable) had no test in the tree at all, so dropping the reserve_w= keyword from the SolarController(...) call passed every one of 235 tests while silently defaulting every restart to 0 W. Added test_async_setup_entry_seeds_the_controllers_reserve_from_options in test_init_entry.py for the read half, and renamed the existing number-entity test to test_setting_the_reserve_writes_it_to_config_ entry_options so its name matches the write-only half it actually covers. The solar select's refusal was only exercised for "active"; narrowing the guard to option == "active" still passed everything, letting "simulate" arm with no grid sensors configured. Extended test_solar_select_refuses_to_arm_without_sensors to check both non-off options. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The ten-minute stop delay was counted from the moment the five-minute average admitted the drop, several minutes after the drop itself, and the car imported at up to the charger's ceiling in between. The raw reading now anchors that clock; the smoothed figure still decides what to do. Rising surplus still waits for the tick, and so does most of the work on a collapse: the fast path fires once per collapse and never more often than the tick would, because the twenty-command hourly backstop is a backstop against bugs and has to still be there for the stop. The tick is no longer re-entrant, now that a sensor event can reach it while an API call is in flight. A blind period (sensors briefly unreadable) also clears the new _collapsed_since anchor, alongside the two clocks it already cleared — without this, a stale anchor from before the blind period let the stop clock resume counting through time nobody actually observed, and tripped an existing regression test. tests/test_init_entry.py also loads solar_controller.py at runtime (via coordinator/solar_controller wiring), so its own homeassistant.helpers.event stub needed the same async_track_state_change_event addition already required for test_entities.py. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nate Both tests, as originally written, passed against a broken implementation: the first's collapse and evaluation landed in the same tick, so the anchor and that tick's own `now` were numerically identical at assertion time; the second's six repeats spanned only 60s, inside the fast path's own one-tick spacing guard, so that guard alone (not the latch under test) held the command count flat. Retimed both against the same real behaviour they already exercised: the first now defers the sensor event's own evaluation past the spacing guard, so the stop clock's anchor and the deferred tick's `now` are forced apart; the second now spaces repeats a tick-and-a-bit apart, past the spacing guard, so only the latch can hold the count flat. Confirmed by mutation: reverting the anchor to plain `now` now fails the first with a mismatched timestamp, and removing the latch's guard now fails the second with the message it names. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix round 2 review findings, all confirmed by mutation: - async_sensor_changed checked only self._mode, not self._stopped. async_stop cancels the sensor subscription but not atomically with setting the flag, so an event already dispatched could still land here in the gap and run a full evaluation - issuing a command - on a torn-down controller. Mirrors _schedule_tick's own _stopped re-arm check. - Three of _collapsed_since's four clearing sites had no test: disarm's (a stale anchor lets a re-arm an hour later issue an immediate STOP on the strength of a collapse from before the disarm), the fast path's minimum-spacing guard (the latch alone cannot stop a raw reading oscillating across the floor faster than one tick, since each down-crossing re-arms it), and _track_thresholds's own clear on recovery (without it, once _collapsed_since has ever been set by a plain tick, "available >= floor and collapsed_since is None" can never be taken again and the controller can never start a charge again). - _last_evaluation, the fast path's own spacing clock, was set before the blind-period return - so a tick that read nothing still reset it, deferring a genuine collapse in the next TICK_SECONDS on the strength of a cycle that observed nothing. Moved after the return. Added test_async_stop_cancels_the_sensor_listener, test_a_sensor_event_after_stop_does_not_evaluate, test_a_fresh_collapse_within_one_tick_still_waits_for_the_spacing_guard, test_a_tick_only_recovery_clears_the_collapse_anchor, and test_a_blind_tick_does_not_reset_the_fast_paths_spacing_clock; extended test_disarming_clears_the_clocks_a_rearm_would_misread with the _collapsed_since case.
A three-phase supply feeding a single-phase charger reports surplus netted across phases, most of which the charger cannot reach. The Daze payload does not say how many phases feed the house — its one phase field describes the charger — so the options flow asks, with no default, and solar control will not arm until it is answered. One property now answers 'can this run, and if not, why not', for the select's availability, its refusal to arm, and the log line. Eco mode and a configured charger schedule are part of that answer, as the spec asks; previously they produced a decision of 'nothing' logged at debug and no other sign. Availability alone was never enough: a service call reaches async_select_option whatever the entity reports. The rate limit is logged at warning rather than debug. It is a backstop against bugs, so if it is what is holding the charger back, that is not a debug-level fact. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ree more from review round 1
The schedule refusal and _build_state's schedule_set both read
data.get("schedules") — a key merge_payload's own _scalars step drops
from every source, because it is a list. Only nextScheduleInfo survives
by name, an object when a schedule is configured and null when it is
not (sensor_catalog.py:62, tests/test_payload.py's schedule-object
tests). The old guard could never fire on real data, and its test only
passed because it injected "schedules" into a flat dict rather than
routing it through merge_payload — the exact supplyGrid3F shape this
task exists to remove. Both sites now read nextScheduleInfo, and the
schedule test is rebuilt on top of a real merge_payload call.
The phase-mismatch check now distinguishes "the charger reported
single-phase" (is False) from "the charger did not report" (is None),
each with its own message, matching the same distinction _car_draw_w,
_read_power and charger_reachable already draw elsewhere. A captured
real payload does carry evseIsThreePhase (tests/test_payload.py's
EVSE_RECORD), so this is a genuine defensive improvement, not a guess
about missing data.
The tick's own stand-down was untested: Task 10 restores the mode
directly at startup, bypassing async_select_option's refusal entirely,
so a three-phase house with a single-phase charger and a restored
ACTIVE mode had no test proving the tick would not drive the charger to
its ceiling on the strength of a netted surplus. Added, and confirmed
to fail without the guard.
"Off" being un-refusable had no test either, despite being the brief's
own named trap: a charger refused for any reason could otherwise never
be turned off again until the refusal cleared.
Saving the options form replaced the options outright, dropping the
solar reserve (written there directly by the reserve entity, with no
field of its own on this form) back to 0 W on every save. Latent
before this task; this task gives every existing user a reason to
reopen the form, since solar control now refuses to arm until the
supply question is answered. Now merged rather than replaced, with a
new tests/test_config_flow.py exercising the real handler against
stubbed Home Assistant and voluptuous, added to run_all.py.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…review round 2
I4's blanket merge ({**options, **user_input}) fixed the solar reserve
but broke something the spec calls out by name: the two grid sensors and
the supply-phases question are all vol.Optional with no default, so the
frontend omits a cleared field from user_input entirely rather than
submitting it empty. The blanket merge read that omission as "unchanged"
and silently restored the stale value from the old options — a sensor
pointed at a renamed or deleted entity_id could never be cleared again
once set, even though the spec calls empty a supported configuration.
Now only the one key this form does not own — the solar reserve, written
directly to these options by Task 7's reserve entity — is carried
forward; everything the form does own is taken exactly as submitted.
tests/test_config_flow.py gains a test that clears a previously-set
sensor and asserts the key is actually gone, which neither existing test
(both of which submit a field rather than clearing one) could catch;
confirmed to fail against the blanket-merge version.
Also: _build_state's own nextScheduleInfo read was reachable by no test
in the tick path, since unsupported_reason returns before decide() is
ever reached — reverting it to the old "schedules" key passed 66 of 67
tests in the file. Added a direct test against a real merge_payload call
that fails on exactly that reversion, confirmed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Timers begin at zero after a restart, so a charge that was already running would read as having no elapsed run time and could be stopped moments after boot. A running charge now seeds its own start time. Once per charge rather than once per tick, and released when the charge is observed to end. Re-seeding on every tick would re-issue a stop that was queued but never landed, every two minutes until the hourly backstop tripped; seeding only once per lifetime would strand the next charge instead, with a clock that reads as zero for ever and can never be stopped. This clock has now been wrong in both directions, which is why it is tested in both. The same flag is released on disarm() too, so a re-arm onto a charge that never stopped can reseed rather than inheriting a clock that was just zeroed and a flag that says not to touch it again. The control also remembers its mode across a restart via RestoreEntity. No stored state at all is a different case from a restart — the select's first run, per the spec's Rollout section — and lands the mode in simulate rather than leaving it at the controller's off default. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rollout item 1 read 'ship with solar control defaulting to off', but the select sets simulate on its first add, so off is never what a user sees. Both facts are true of different objects — the controller ships off, the entity lands on simulate — and the line collapsed them into one claim that the README was about to repeat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix round 1 on Task 10. Four gaps the review found untestable by the existing suite, none requiring a production change: - The seeded flag's reset was never exercised: the existing test starts from a charger that was never charging, so the reset never has to fire. Added a test that seeds a real episode, lets solar stop it, observes the charger idle, and starts a second episode — the sequence that actually needs the release. - The inner `if self._started_at is None` guard, named in the brief, had nothing defending it. Added a test driving a solar-issued START through to the next tick and asserting the mark is not backdated. - The restore path's `last.state in SOLAR_MODE_OPTIONS` membership check was unguarded: an unrecognised stored state (`"unavailable"`, a state Task 9 makes newly reachable) would raise ValueError out of async_added_to_hass and take the entity down with it. - The restore path's direct assignment, bypassing async_select_option, had no assertion of its own beyond the inference from Task 9's stand-down test. Added a test restoring into an unsupported setup and asserting it does not raise. All four verified by mutation: broken, confirmed the named test failed with the reviewer's own reproduction, restored with a targeted edit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Includes the simulate-first procedure, because a feature that starts and stops the car should be watched for a day before it is trusted, and what makes the control unavailable, because a feature that refuses to arm has to say why somewhere a user will look. Corrects four points found in review after the task brief was written: a fresh install shows simulate, not off; entity IDs are examples, not guaranteed, since nothing in this integration sets a translation key or explicit name; the supply-phase declaration is a third options-flow field with no default, and existing installs will find solar control refusing to arm until they answer it; and the validation-day checklist now names the specific checks, including the one guard whose positive direction has never been observed on real hardware. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
rules.md holds this project's QA and working conventions, adapted from the WebConsole runbook. It stays local by operator decision: the entry is here rather than in .git/info/exclude so that any clone of this repository inherits the protection, since the incident it guards against was a git add -A. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A fresh whole-branch review (final-review.md) found five defects that live between tasks rather than inside any one of them, all local to solar_controller.py and solar.py: - C1: a STOP only queued for the background retry left _charge_seeded True forever if that retry chain exhausted, stranding the min-run clock at 0.0 and leaving the car importing from the grid indefinitely. _send_command now takes an on_retry_exhausted hook; the STOP send releases _charge_seeded when the retry gives up. - C2: the mode setter cleared only 3 of 8 episode clocks while disarm() cleared 7, so a select round-trip (off, then back to active) stranded _collapsed_since, _started_at, _charge_seeded and _backoff_until. Extracted _reset_episode(), now called by both the setter and disarm(). - I1: a raw dip below the floor (an oven cycling, say) reset _above_since even when the smoothed surplus never left the healthy range, so the 300s start delay could never accrue. Split the combined if/else in _track_thresholds into two independent conditions, so _above_since answers only to the smoothed figure while _below_since's existing anchor behaviour is preserved exactly (it is the De Morgan negation of the old combined condition). - I2: _carry_out did not re-test the mode across its own awaits, so a disarm landing mid-START (e.g. the power slider) still let the start-charge send through afterwards. Added a mode re-check before that send. - I3: the ignored-start back-off suppressed STOP unconditionally for the full hour, including once the car started drawing on its own. Gated the guard on `not state.charging`. Each fix carries a new test in tests/test_solar_controller.py that fails under its own reverted mutation (verified individually before this commit) and the existing 272 tests are unchanged and passing. 272 -> 277 passed across 8 modules; ruff check clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A byte-identical 253 KB copy of another project's rules.md appeared at this repo root as .rules.md, untracked and uncovered by the /rules.md entry. It holds no credentials, but it is another project's internal incident history and infrastructure detail, and a git add -A would have committed it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_build_state coerced maxExternalChargingCurrentInMilliAmps and then discarded the result with 'or 0.0', so an absent or unparseable limit entered SolarState as 0 W. current_limit_w is what decide() compares the target against across the 300 W deadband, so every target then looked like a large change and the first tick issued a SET to re-assert a limit that was most likely already correct, spending one of the twenty commands an hour on an unknown. The cycle is now skipped when the charger has not reported a limit, with its own warning flag. The threshold clocks are deliberately left alone, unlike the blind-sensor path above: this cycle did observe the surplus, and only the charger's own limit is missing. Found by running rules.md section 5 over the branch after the final review had passed it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
arest
requested changes
Sep 30, 2026
arest
left a comment
Owner
There was a problem hiding this comment.
Could we not change the references to the project owner ?
| "name": "Daze Wallbox Integration", | ||
| "codeowners": ["@arest"], | ||
| "codeowners": [ | ||
| "@tarrinho" |
| "config_flow": true, | ||
| "dependencies": [], | ||
| "documentation": "https://github.com/arest/daze-addon", | ||
| "documentation": "https://github.com/tarrinho/daze-addon", |
| "integration_type": "device", | ||
| "iot_class": "cloud_polling", | ||
| "issue_tracker": "https://github.com/arest/daze-addon/issues", | ||
| "issue_tracker": "https://github.com/tarrinho/daze-addon/issues", |
| [](https://www.home-assistant.io/) | ||
| [](https://github.com/arest/daze-addon/actions/workflows/validate.yaml) | ||
| [](LICENSE) | ||
| [](https://github.com/tarrinho/daze-addon/actions/workflows/validate.yaml) |
| [](https://github.com/arest/daze-addon/actions/workflows/validate.yaml) | ||
| [](LICENSE) | ||
| [](https://github.com/tarrinho/daze-addon/actions/workflows/validate.yaml) | ||
| [](LICENSE) |
|
|
||
| Daze wallboxes are managed through the [Daze web portal](https://webportal.dazeservice.com). This integration bridges the gap, bringing your wallbox into Home Assistant alongside all your other smart home devices. | ||
|
|
||
| > **This is a fork.** The original integration was created by **Andrea Restello** ([@arest](https://github.com/arest)) at [arest/daze-addon](https://github.com/arest/daze-addon), and all of the original design and implementation is his work. This fork, maintained by **Pedro Tarrinho** ([@tarrinho](https://github.com/tarrinho)), adds fixes found while running it against a DT01 charger — see [Changes in this fork](#changes-in-this-fork). |
| 4. Add this repository URL: | ||
| ``` | ||
| https://github.com/arest/daze-addon | ||
| https://github.com/tarrinho/daze-addon |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I hope to fix the issue reported.
Please check!