[Guide] Integrating the Olight Ostation in Home Assistant
Hey everyone, I recently picked up the Olight Ostation 2 Smart 3-in-1 Battery Charger and wanted to get it fully integrated into Home Assistant. It’s a great piece of hardware! However, the stock Olight app isn’t my favorite, so getting this running locally via a Tuya Local Wi-Fi implementation was a priority.
Quick vs. Advanced Implementation
- The Advanced Method: If you want voltage, current, and status, you need template sensors to decode the hex-encoded packets, and the UI below makes that look pretty.
- The Super Quick Method: If you want a super quick implementation, just drop the device YAML into your
tuya_localdevice folder. You don’t need the UI and template sensors below if you don’t care about more than on/off and basic stuff.
Prerequisites: What You Will Need
Before you begin, you will need to extract three specific pieces of information to talk to this device locally on your network:
- DEVICE_ID: Your unique 22-character device ID.
- IP_ADDRESS: The local IP address of your charger on your network (e.g.,
192.168.1.X). - LOCAL_KEY: Your 16-character local encryption key.
Getting the Local Key
Connect your phone in USB debug mode. Running ADB, you can look at the logs and the Tuya SDK (labeled Thing-Network in your logs) has a debug leak where it prints the active localKey to the system log immediately before calculating the session key.
adb logcat -v time Thing-Network:D Thing-Bluetooth:D Thing-Activator:D *:S
Watch the logs to pull your 16-character localKey. Note: I’m pretty sure the BT and Wi-Fi local keys are different, but the Wi-Fi was the quickest way to get this working.
Step 1: Tuya Local Instructions
- Install Tuya Local: This is a Home Assistant integration via HACS to support devices running Tuya firmware without going via the Tuya cloud. Devices are supported over WiFi. Note that a reboot of Home Assistant is necessary after installing HACS components. Also, note that many Tuya devices seem to support only one local connection. I haven’t had any issue with Ostation integration running and my phone.
- Add the Custom Device Profile: Copy the following YAML and save it as
/custom_components/tuya_local/devices/olight_ostation_charger.yaml. Restart HA.
Click to expand olight_ostation_charger.yaml
name: Charger
products:
- id: 1fnensm6
manufacturer: Olight
model: Ostation
- id: v8znljza
manufacturer: Olight
model: Ostation
entities:
- entity: switch
name: Power
dps:
- id: 101
type: boolean
name: switch
- entity: sensor
name: Fully charged count
category: diagnostic
dps:
- id: 102
type: integer
name: sensor
- entity: binary_sensor
name: DP103
category: diagnostic
dps:
- id: 103
type: boolean
name: sensor
- entity: binary_sensor
name: DP104
category: diagnostic
dps:
- id: 104
type: boolean
name: sensor
- entity: sensor
name: DP105
category: diagnostic
dps:
- id: 105
type: integer
name: sensor
- entity: binary_sensor
name: DP106
category: diagnostic
dps:
- id: 106
type: boolean
name: sensor
- entity: sensor
name: Invalid battery count
category: diagnostic
dps:
- id: 108
type: integer
name: sensor
- entity: sensor
name: Raw Bay Telemetry
category: diagnostic
dps:
- id: 110
type: string
name: sensor
optional: true
- entity: switch
name: Pause schedule
dps:
- id: 111
type: boolean
name: switch
- entity: sensor
name: Pause Window Raw
category: diagnostic
dps:
- id: 112
type: string
name: sensor
optional: true
- entity: sensor
name: DP113
category: diagnostic
dps:
- id: 113
type: integer
name: sensor
- Configure the Integration: Go into Devices & Services on HA and MANUALLY add a new Tuya Local device.
CRITICAL: Be absolutely sure to pick 3.5 interface for Tuya. Put in your local key that you got via ADB, your device ID, and IP. THEN you will be asked which device interface, andCharger (Ostation)will show up. Now you can simply add the result to your UI.
Step 2: Advanced Telemetry Decoding (Template Sensors)
The DP Slot Keys
For reference, here are the core DP (Data Point) keys the charger uses:
- 101: Global Power State (Boolean)
- 102: “Fully Charged” Counter (Matches the app GUI)
- 108: “Invalid Batteries” Counter
- 110: Live Multi-Bay Telemetry (The Hex Payload)
- 111 / 112: Paused Charging Schedule Controls
The Hex Scheme (DP 110 Chunk Map)
When the Ostation sends DP 110, it sends a 68-byte hex string containing four 17-byte chunks. The slots are strictly ordered:
- Bytes 0-16: Telemetry for Slot 3
- Bytes 17-33: Telemetry for Slot 4
- Bytes 34-50: Telemetry for Slot 1
- Bytes 51-67: Telemetry for Slot 2
Slots 1 through 4 data are hex encoded packets. So if you want to decode all that, I included template sensors below that decode raw strings on packet 110 into charge type, voltage, etc. I don’t have lithium batteries or olight batteries to read likely other fields.
I’m not bothering to do anything with the on/off scheduler since you can do that control in HA anyway.
Within every 17-byte chunk, the bytes decode like this:
- Byte 0: Slot ID (01, 02, 03, 04)
- Byte 1: Chemistry (00 = Detecting, 01 = Ni-MH)
- Byte 2: Battery % (00 - 64 Hex → 0% to 100%)
- Byte 3: Error Flags
- Byte 4: Charge cycle count (counts up charge cycles for ea battery)
- Bytes 8-9: Voltage (Big-Endian Int, raw mV)
- Bytes 10-11: Current (Big-Endian Int, requires 10x multiplier for mA)
- Byte 12: Temperature (°C)
Add this to your configuration.yaml. This code will break the hex encoded data into the schema above.**
Click to expand Template Sensors
template:
- sensor:
#########################################################
# SLOT 3 (bytes 0–16)
#########################################################
- name: Ostation Slot 3 ID
unique_id: ostation_slot_3_id
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[0] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 3 Chemistry
unique_id: ostation_slot_3_chemistry
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[1] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 3 Percent
unique_id: ostation_slot_3_percent
unit_of_measurement: "%"
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[2] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 3 Flags
unique_id: ostation_slot_3_flags
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[3] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 3 Charge State
unique_id: ostation_slot_3_charge_state
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[4] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 3 Voltage
unique_id: ostation_slot_3_voltage
unit_of_measurement: "mV"
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}
{{ (b[8] * 256) + b[9] }}
{% else %}
{{ none }}
{% endif %}
{% else %}
{{ none }}
{% endif %}
- name: Ostation Slot 3 Current
unique_id: ostation_slot_3_current
unit_of_measurement: "mA"
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}
{{ ((b[11] * 256) + b[10]) * 10 }}
{% else %}
{{ none }}
{% endif %}
{% else %}
{{ none }}
{% endif %}
- name: Ostation Slot 3 Temperature
unique_id: ostation_slot_3_temperature
unit_of_measurement: "°C"
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[12] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
#########################################################
# SLOT 4 (bytes 17–33)
#########################################################
- name: Ostation Slot 4 ID
unique_id: ostation_slot_4_id
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[17] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 4 Chemistry
unique_id: ostation_slot_4_chemistry
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[18] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 4 Percent
unique_id: ostation_slot_4_percent
unit_of_measurement: "%"
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[19] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 4 Flags
unique_id: ostation_slot_4_flags
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[20] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 4 Charge State
unique_id: ostation_slot_4_charge_state
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[21] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 4 Voltage
unique_id: ostation_slot_4_voltage
unit_of_measurement: "mV"
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}
{{ (b[25] * 256) + b[26] }}
{% else %}
{{ none }}
{% endif %}
{% else %}
{{ none }}
{% endif %}
- name: Ostation Slot 4 Current
unique_id: ostation_slot_4_current
unit_of_measurement: "mA"
state: >
{% set raw =
---
### What the charger actually does (Observed Behavior)
Summary: The Ostation takes about 3 minutes of "probing" a battery (reading voltage and checking chemistry) before it officially flips into an active charging state and reports current/temp telemetry. You can see the voltages it's reading even when it's not charging.
Cells are physically moved in paired slots (AA and AAA separately). Within a pair, both cells must complete charging before the systestates('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}
{{ ((b[28] * 256) + b[27]) * 10 }}
{% else %}
{{ none }}
{% endif %}
{% else %}
{{ none }}
{% endif %}
- name: Ostation Slot 4 Temperature
unique_id: ostation_slot_4_temperature
unit_of_measurement: "°C"
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[29] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
#########################################################
# SLOT 1 (bytes 34–50)
#########################################################
- name: Ostation Slot 1 ID
unique_id: ostation_slot_1_id
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[34] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 1 Chemistry
unique_id: ostation_slot_1_chemistry
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[35] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 1 Percent
unique_id: ostation_slot_1_percent
unit_of_measurement: "%"
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[36] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 1 Flags
unique_id: ostation_slot_1_flags
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[37] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 1 Charge State
unique_id: ostation_slot_1_charge_state
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[38] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 1 Voltage
unique_id: ostation_slot_1_voltage
unit_of_measurement: "mV"
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}
{{ (b[42] * 256) + b[43] }}
{% else %}
{{ none }}
{% endif %}
{% else %}
{{ none }}
{% endif %}
- name: Ostation Slot 1 Current
unique_id: ostation_slot_1_current
unit_of_measurement: "mA"
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}
{{ ((b[45] * 256) + b[44]) * 10 }}
{% else %}
{{ none }}
{% endif %}
{% else %}
{{ none }}
{% endif %}
- name: Ostation Slot 1 Temperature
unique_id: ostation_slot_1_temperature
unit_of_measurement: "°C"
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[46] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
#########################################################
# SLOT 2 (bytes 51–67)
#########################################################
- name: Ostation Slot 2 ID
unique_id: ostation_slot_2_id
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[51] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 2 Chemistry
unique_id: ostation_slot_2_chemistry
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[52] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 2 Percent
unique_id: ostation_slot_2_percent
unit_of_measurement: "%"
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[53] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 2 Flags
unique_id: ostation_slot_2_flags
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[54] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 2 Charge State
unique_id: ostation_slot_2_charge_state
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[55] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
- name: Ostation Slot 2 Voltage
unique_id: ostation_slot_2_voltage
unit_of_measurement: "mV"
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}
{{ (b[59] * 256) + b[60] }}
{% else %}
{{ none }}
{% endif %}
{% else %}
{{ none }}
{% endif %}
- name: Ostation Slot 2 Current
unique_id: ostation_slot_2_current
unit_of_measurement: "mA"
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}
{{ ((b[62] * 256) + b[61]) * 10 }}
{% else %}
{{ none }}
{% endif %}
{% else %}
{{ none }}
{% endif %}
- name: Ostation Slot 2 Temperature
unique_id: ostation_slot_2_temperature
unit_of_measurement: "°C"
state: >
{% set raw = states('sensor.ostation_charger_raw_bay_telemetry') %}
{% if raw not in ['unknown','unavailable','',None] %}
{% set b = raw | base64_decode(none) %}
{% if b | length >= 68 %}{{ b[63] }}{% else %}{{ none }}{% endif %}
{% else %}{{ none }}{% endif %}
CONTINUED




