Releases: lightheaded/ha-skyrc
Release list
v0.4.0b5
Beta.
- New: Channel X delete preset on every channel. It deletes the preset that
channel's select is showing — one row above the button — so no name has to be
retyped. It is greyed out while the channel is not set to a preset. - Presets stay charger-wide: deleting from one channel removes it everywhere,
asskyrc.delete_presetalways did.
Unchanged from b4: Channel X preset name plus Channel X save preset,
per channel; storage runs report charging/discharging; the options and reauth
dialogs load; entered values survive program changes and disconnects; and the
config-side entities load with the charger out of Bluetooth range.
v0.4.0b4
Beta. Reverses the preset-saving change in b3, keeping the part that was worth
having.
- Channel X preset name (a field that keeps what you type) and Channel X
save preset (the button that spends it) — both per channel, sorted into
that channel's own block beside its preset select. - b3's save-on-enter field is gone. It also never cleared itself as claimed:
its state was always empty, so a save changed nothing and the frontend was
never told to redraw. - Entities from b1 and b3 are removed from the registry on setup.
Unchanged from b1/b2: storage runs report charging/discharging, the options and
reauth dialogs load, entered values survive program changes and disconnects,
and the config-side entities load with the charger out of Bluetooth range.
v0.4.0b3
Beta. Fixes the preset flow's discoverability.
- The charger-wide Preset name field and the per-channel save preset
buttons are replaced by one field per channel: Channel X save preset as.
Type a name, press enter — the staged program is saved and the field clears.
It now sits next to that channel's preset select instead of forty rows
away in the alphabetically sorted configuration list. - The entities it replaces are removed from the registry automatically.
Everything from 0.4.0b1/b2 is unchanged: storage runs report
charging/discharging, the options and reauth dialogs load, entered values are
kept across program changes and disconnects, and the config-side entities load
even when the charger is out of Bluetooth range.
v0.4.0b2 — set up with the charger off (beta)
Second beta. Everything in v0.4.0b1 — presets, kept values, the direction of a storage run, and the 500 on Configure — plus one fix that b1 made obvious.
Fixed: nothing appeared while the charger was out of range
A charger switched off or away from a proxy left the entry in Retrying setup with "not currently in range of a Bluetooth adapter", and no entity was created at all — the first poll had to succeed before any platform loaded. On b1 that showed up as the new preset entities simply not being there.
The staged programs and presets are Home Assistant's state, not the charger's, so they no longer wait for it:
- usable with the charger off — battery type, program, the staged numbers, the preset select, save preset, preset name
- unavailable until a poll lands — the channel sensors, charger temperature, start and stop, the beep volume and the charger's own settings
The coordinator keeps polling on its usual 30 s cycle, so the entry picks itself up when the charger comes back. Stage a program while the charger is off and it is waiting for you when it powers on.
Still a beta
None of this has run against a charger yet. The direction of a storage run is the part I would most like confirmed on real hardware, and writes over an ESPHome Bluetooth proxy are still untested.
Upgrading from 0.3.0? Your staged values reset once — see the b1 notes.
v0.4.0b1 — presets, kept values, storage direction (beta)
A beta. Everything below is verified in unit tests and against Home Assistant's own entity and config-flow machinery, but not yet on a real charger. Please report what you see — especially over an ESPHome Bluetooth proxy.
Install it from HACS by ticking Show beta versions on the integration, then picking 0.4.0b1. To go back: pick 0.3.0 and restart.
Everything here comes from egiljae's report — thank you.
Fixed: "Config flow could not be loaded: 500 Internal Server Error"
The gear icon on the integration page did nothing but throw a 500, and the passcode prompt a refusing charger raises would have done the same. Both forms validated the passcode with a regex validator that Home Assistant cannot render, so the form never loaded. A wrong code now comes back as an error on the field.
Fixed: a storage run reading "Working"
Nothing in the protocol says whether a storage or cycle program is charging or discharging — the working-state byte reads the same either way, which is why both showed as working while the attributes correctly said storage.
The pack voltage says it instead: it rises on a charge and falls on a discharge. A 10 mV move across the pack settles the matter, measured against an anchor rather than the previous poll, so a storage discharge crawling a millivolt at a time still adds up instead of vanishing into the deadband, and a cycle turning round is caught within a poll or two. Expect a poll or two of working at the start of a run.
A storage run started from Home Assistant does not wait for that: its setpoint is known exactly, so the direction follows from the voltage the pack starts at.
Bonus: a channel whose program is unknown at all — a charger that never answers the basic-info query, or one with Read the program of a running channel turned off — now gets a direction too, where it used to be stuck on working.
New: what you type is kept
Staged values used to live in memory and revert to the charger's defaults whenever anything moved. Now:
- a value entered for a program comes back when you return to that program
- currents carry over to whichever program you switch to
- everything is written to
.storage, so a lost Bluetooth link, a reload or a restart no longer costs you the numbers
Voltage setpoints deliberately do not carry across a battery-type change — they belong to the chemistry, and folding a 4200 mV LiPo setpoint into the LiFe range would quietly land on that range's maximum.
New: presets
A whole staged program under a name — battery type, program, cell count, currents and voltages — saved on the charger and applied to any channel:
- Stage the program you want on a channel.
- Type a name in the new Preset name field.
- Press Channel X save preset.
From then on that channel's Preset select stages the lot in one pick. Editing any parameter afterwards leaves the select blank again — what is staged is no longer the preset. Saving over a name replaces it.
For automations: skyrc.save_preset and skyrc.delete_preset, both targeting a channel's preset select.
New entities: select.…_channel_X_preset, button.…_channel_X_save_preset, text.…_preset_name.
Upgrading
Your staged values reset once, to the charger's defaults, on the first start after upgrading — the per-entity state restore they used to rely on is gone in favour of storing the whole staged program at once, which is what makes them survive a charger that drops off Bluetooth. Nothing else changes; no entity is renamed or removed.
v0.3.0 — charger control
This release adds control. Before, the integration only read the charger.
Control a channel
Each channel gets a staged program and two buttons. Set the battery type, the
program, the cell count, the currents and the per-cell voltages. Then press
start. The stop button stops the channel, and clears a channel that shows an
error.
For automations, use the skyrc.start_program action. It runs one program in a
single call. Values you do not give come from the staged program.
The ranges follow the charger. They change with the battery type and the
program. A parameter that the program does not use shows as unavailable.
CAUTION: A charger that starts from Home Assistant can start when you are not
there. The manual is clear about this: never leave batteries unattended while
they charge. The charger accepts almost any value it is sent. A live Q200neo
accepted a charge voltage of 9999 mV per cell. This integration holds the same
limits as the SkyCharger app, but a program that is sent to the wrong pack still
runs.
Charger settings
The settings behind Task Parameters and System Settings are now entities: the
safety timer, the capacity cut-off, the minimum input voltage, the maximum input
power, the beep volume and the completion beep. These settings are global, so
there is one entity for each.
The safety timer and the capacity cut-off are the important two. They are what
stops a run that goes wrong while nobody watches it.
The PASSCODE screen
Some chargers show PASSCODE: NNNN on the display while Home Assistant reads
them. The cause is now known. The charger refuses the passcode that Home
Assistant sends, and shows the code that it wants.
If this happens, Home Assistant asks you for the code. Read the four digits from
the charger and type them in. You can also set the code in the options, or stop
the query that carries it.
The screen also came up more often than necessary, because the integration asked
for the program of every running channel on every poll. It now asks one time for
each run.
Bug fix: channel A
The charger discards the first command after a connection starts. Because the
integration reads the channels in order, channel A lost its reading on every
poll. The integration now waits 0.5 s after it starts notifications.
Notes
All protocol facts in this release were tested against a Q200neo, with and
without a battery pack. See PROTOCOL.md for the details.
Three of these facts correct the reference app: the write mode of the
characteristic, the layout of the START_CHARGE reply, and the meaning of the
passcode flag.
v0.2.0 — Charging / discharging per channel
The channel status sensor now says which way the current flows, and replaces the per-channel charging binary sensor.
Requested on the Home Assistant community forum — thanks to egiljae for the suggestion.
Highlights
- Status reports
charginganddischargingin place ofworking, whenever the charger tells us which way the current flows. - New attributes on the status sensor:
battery_type(lipo,liion,nimh, …) andprogram(balance_charge,discharge,storage,cycle, …), so a "done" notification can say what finished. - The direction comes from
QUERY_BASIC_INFO(0x5F), newly documented in PROTOCOL.md. It is only queried while a channel is working (plus once for a finished channel), so idle channels cost no extra BLE round trips.
⚠️ Breaking change — the charging binary sensor is gone
The per-channel binary_sensor.<charger>_channel_<X>_charging entities are removed; the status sensor carries that information now. They are deleted from the entity registry on the first start after upgrading, so they will not linger as unavailable.
Automations that trigger on the status sensor going to: "done" are unaffected — including the example below.
Do I need to change anything?
Only if something referenced the binary sensor. Find out before upgrading — Developer tools → Template:
{{ integration_entities('skyrc') | select('match', 'binary_sensor') | list }}Then check each entity's Related tab, or search your YAML for _charging.
Replacing a reference
"This channel is running" is now three status states — charging, discharging, and working (see below):
# before
triggers:
- trigger: state
entity_id: binary_sensor.charger_8f12_channel_a_charging
to: "on"
# after
triggers:
- trigger: state
entity_id: sensor.charger_8f12_channel_a_status
to: ["charging", "discharging", "working"]{# before #}
{{ is_state('binary_sensor.charger_8f12_channel_a_charging', 'on') }}
{# after #}
{{ states('sensor.charger_8f12_channel_a_status') in ['charging', 'discharging', 'working'] }}On a dashboard, replace the binary-sensor row with the status sensor; the old row shows Entity not available until you do.
If you already match on the working state
Anything comparing the status sensor to working will silently stop matching a channel whose direction is now known — it reads charging or discharging instead. Use the three-state list above.
Why three states and not two
The charger reports one working state for both directions; the direction is derived from the channel's program. Two cases keep reporting plain working rather than guess:
- The storage and cycle programs, which charge or discharge depending on where the pack starts.
- Chargers that do not answer the basic-info query at all.
A SkyCharger passcode does not need to be cleared
Verified on a Q200neo with an app passcode set: both 0x55 and 0x5F are answered normally, and the charger reports no password check (d[9] = 0x00). An app passcode is not the per-channel passwordEnable flag the protocol cares about. The README previously listed clearing it as a requirement — that has been corrected, and PROTOCOL.md now records what was actually observed. A channel with passwordEnable genuinely set remains untested; if it ever refuses, the client stops asking after three timeouts and the direction falls back to working.
A richer "channel finished" notification
battery_type and program are kept while a channel works and after it finishes (they clear when it returns to idle), so a notification can report what finished, the pack, and how well the cells are balanced:
🔋 Channel B at storage voltage
Li-ion 6S · 22.56 V · 32 mAh
Cells 3.753–3.763 V (Δ10 mV)
alias: SkyRC — notify when a channel finishes
mode: queued
triggers:
- trigger: state
entity_id:
- sensor.charger_8f12_channel_a_status
- sensor.charger_8f12_channel_b_status
- sensor.charger_8f12_channel_c_status
- sensor.charger_8f12_channel_d_status
to: "done"
# Don't re-fire when a channel comes back from a failed BLE poll.
not_from: ["unavailable", "unknown"]
actions:
- action: notify.main # or notify.mobile_app_your_phone
data:
title: >-
{%- set eid = trigger.entity_id -%}
{%- set ch = eid.split('_channel_')[1].split('_status')[0] | upper -%}
{%- set prog = state_attr(eid, 'program') -%}
{%- set verb = {'charge': 'charged', 'balance_charge': 'charged',
'fast_charge': 'charged', 'auto_charge': 'charged', 're_peak': 're-peaked',
'discharge': 'discharged', 'storage': 'at storage voltage',
'cycle': 'cycled'}.get(prog, 'finished') -%}
🔋 Channel {{ ch }} {{ verb }}
message: >-
{%- set eid = trigger.entity_id -%}
{%- set base = eid.replace('_status', '') -%}
{%- set batt = {'lipo': 'LiPo', 'liion': 'Li-ion', 'life': 'LiFe',
'lihv': 'LiHV', 'nimh': 'NiMH', 'nicd': 'NiCd', 'pb': 'Pb',
'pb_agm': 'Pb AGM'}.get(state_attr(eid, 'battery_type')) -%}
{%- set cells = state_attr(eid, 'cell_configuration') -%}
{%- set cv = state_attr(eid, 'cell_voltages_mv') or [] -%}
{%- set v = states(base ~ '_voltage') | float(0) -%}
{%- set mah = states(base ~ '_capacity') | int(0) -%}
{%- set t = states(base ~ '_battery_temperature') -%}
{%- set spread = (cv | max - cv | min) if cv | count > 1 else 0 -%}
{{ [batt, cells] | select('string') | join(' ') }}{% if batt or cells %} · {% endif %}{{ v | round(2) }} V{% if mah > 0 %} · {{ mah }} mAh{% endif %}{% if cv | count > 1 %}{{ '\n' }}Cells {{ (cv | min / 1000) | round(3) }}–{{ (cv | max / 1000) | round(3) }} V (Δ{{ spread }} mV){% if spread > 50 %} ⚠️{% endif %}{% endif %}{% if t not in ['unknown', 'unavailable', 'none'] %}{{ '\n' }}Pack {{ t }} °C{% endif %}Every part degrades independently — the verb falls back to a plain "finished" on an unknown program, and the pack, capacity, cell-spread and temperature parts drop out when the charger doesn't report them. The cell spread carries a
Two things worth copying even if you keep your own message:
not_from: ["unavailable", "unknown"]on the trigger — without it, a channel recovering from a failed BLE poll re-fires the notification.- Elapsed time is deliberately absent: the duration entity is disabled by default, and its unit (seconds vs minutes) is not yet confirmed.
Validation
Verified against a live Q200neo with a 6S Li-ion pack in storage mode: the basic-info frame 02 03 01 06 03 32 14 00 00 00 decoded to Li-ion, 6 cells, storage program, no password — matching the pack on the bench. Poll cycles stayed at 1.3–2.0 s.