Skip to content

v0.2.0 — Charging / discharging per channel

Choose a tag to compare

@lightheaded lightheaded released this 12 Aug 09:51
· 10 commits to master since this release
v0.2.0
9af4160

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 charging and discharging in place of working, whenever the charger tells us which way the current flows.
  • New attributes on the status sensor: battery_type (lipo, liion, nimh, …) and program (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 ⚠️ over 50 mV, which is just a number in the template to tune.

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.

Details and the full diff: #2 (closes #1).