Skip to content

Commit

Permalink
Add new ble_adv_controller component integrating the handling of fan,…
Browse files Browse the repository at this point in the history
… lights, pairing button and new variants (#17)

* Add new ble_adv_component integrating the handling of fan, lights, pair button as well as all encoding for ZhiJia(3) andFanLamp(4)

* Added reversed for ZhiJia. Reviewed Fan and Light param setup algo. Reviewed logging. Reviewed ID computation for ZhiJia.

* Review UUID computation

* Correct Warning during compilation, add Fan direction for fanlamp pro v1

* Add handling of secondary light for FanLamp, remove index option but keep it in the background

* Updated doc to add technical limitations and more details first steps

* Add Secondary light control for ZhiJia

* Workaround for flickering issue

* Add Fan oscillation

* Full review of the advertising process introducing sequencing, dynamic configuration of encoding and variant, multiple encoding and advertising, Fan Oscillation

* Add Dynamic configuration for Duration. Update doc.

* Correct max value for ZhiJia brightness and color temperature, code clean-up, added documentation.

* Revert to raw encoding for fanlamp_pro

* Correct compilation issues on C3 board and esp-idf framework

* restore_mode support for Fan

* Fix Zhi Jia Light flickering issue

* add dynamic configuration for min_brightness

* Add decoder, re align encodings with real apps, refactor encoders to ease future customizations

* Remove legacy components, update doc

* Force direction / oscillation refresh on fan startup

* Adding official support for more Apps

* fix forced oscillation / direction double command on speed change
  • Loading branch information
NicoIIT authored Aug 22, 2024
1 parent d6a3665 commit 43fe118
Show file tree
Hide file tree
Showing 40 changed files with 3,352 additions and 1,246 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 Lev Aronsky
Copyright (c) 2023 NicoIIT

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
34 changes: 22 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
# Lev's ESPHome Components
# BLE ADV ESPHome Components

Custom components for ESPHome
Custom components for ESPHome using BLE Advertising

## Lamps based on BLE Advertising
## Fans / Lamps controlled by BLE Advertising

Use this for various Chinese lamps that are controlled via BLE advertising packets. Supported apps:
Use this for various Chinese lamps that are controlled via BLE advertising packets.
Supported apps:

* LampSmart Pro (tested against Marpou Ceiling Light)
* ZhiJia (tested against aftermarket LED drivers; only the latest version is currently supported)
* LampSmart Pro
* Lamp Smart Pro - Soft Lighting / Smart Lighting
* FanLamp Pro
* ApplianceSmart
* Vmax smart
* Zhi Jia
* Other (Legacy), removed app from play store: 'FanLamp', 'ControlSwitch'

Details can be found [here](components/ble_adv_light/README.md).
Details can be found [here](components/ble_adv_controller/README.md).

## LampSmart Pro (deprecated)

Using this component directly is deprecated, and it will be removed in the future. Please switch to
the above component with the LampSmart Pro configuration.
Used for Marpou Ceiling Light - see details [here](components/lampsmart_pro_light/README.md).
## Credits
Based on the initial work from:
* @MasterDevX, [lampify](https://github.com/MasterDevX/lampify)
* @flicker581, [lampsmart_pro_light](https://github.com/flicker581/esphome-lampsmart)
* @aronsky, [ble_adv_light](https://github.com/aronsky/esphome-components)
* @14roiron, [zhijia encoders](https://github.com/aronsky/esphome-components/issues/11), [investigations](https://github.com/aronsky/esphome-components/issues/18)
* All testers and bug reporters from the initial threads:
* https://community.home-assistant.io/t/controlling-ble-ceiling-light-with-ha/520612/199
* https://github.com/aronsky/esphome-components/pull/17
396 changes: 396 additions & 0 deletions components/ble_adv_controller/CUSTOM.md

Large diffs are not rendered by default.

306 changes: 306 additions & 0 deletions components/ble_adv_controller/README.md

Large diffs are not rendered by default.

289 changes: 289 additions & 0 deletions components/ble_adv_controller/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.core import ID
from esphome.const import (
CONF_DURATION,
CONF_ID,
CONF_NAME,
CONF_REVERSED,
CONF_TYPE,
CONF_INDEX,
CONF_VARIANT,
PLATFORM_ESP32,
)
from esphome.cpp_helpers import setup_entity
from .const import (
CONF_BLE_ADV_CONTROLLER_ID,
CONF_BLE_ADV_ENCODING,
CONF_BLE_ADV_FORCED_ID,
CONF_BLE_ADV_MAX_DURATION,
CONF_BLE_ADV_SEQ_DURATION,
CONF_BLE_ADV_SHOW_CONFIG,
)

AUTO_LOAD = ["esp32_ble", "select", "number"]
DEPENDENCIES = ["esp32"]
MULTI_CONF = True

bleadvcontroller_ns = cg.esphome_ns.namespace('bleadvcontroller')
BleAdvController = bleadvcontroller_ns.class_('BleAdvController', cg.Component, cg.EntityBase)
BleAdvEncoder = bleadvcontroller_ns.class_('BleAdvEncoder')
BleAdvMultiEncoder = bleadvcontroller_ns.class_('BleAdvMultiEncoder', BleAdvEncoder)
BleAdvHandler = bleadvcontroller_ns.class_('BleAdvHandler', cg.Component)
BleAdvEntity = bleadvcontroller_ns.class_('BleAdvEntity', cg.Component)

FanLampEncoderV1 = bleadvcontroller_ns.class_('FanLampEncoderV1')
FanLampEncoderV2 = bleadvcontroller_ns.class_('FanLampEncoderV2')
ZhijiaEncoderV0 = bleadvcontroller_ns.class_('ZhijiaEncoderV0')
ZhijiaEncoderV1 = bleadvcontroller_ns.class_('ZhijiaEncoderV1')
ZhijiaEncoderV2 = bleadvcontroller_ns.class_('ZhijiaEncoderV2')

BLE_ADV_ENCODERS = {
"fanlamp_pro" :{
"variants": {
"v1": {
"class": FanLampEncoderV1,
"args": [ 0x83, False ],
"max_forced_id": 0xFFFFFF,
"ble_param": [ 0x19, 0x03 ],
"header": [0x77, 0xF8],
},
"v2": {
"class": FanLampEncoderV2,
"args": [ [0x10, 0x80, 0x00], 0x0400, False ],
"ble_param": [ 0x19, 0x03 ],
"header": [0xF0, 0x08],
},
"v3": {
"class": FanLampEncoderV2,
"args": [ [0x20, 0x80, 0x00], 0x0400, True ],
"ble_param": [ 0x19, 0x03 ],
"header": [0xF0, 0x08],
},
"v1a": {
"legacy": True,
"msg": "please use 'other - v1a' for exact replacement, or 'fanlamp_pro' v1 / v2 / v3 if effectively using FanLamp Pro app",
},
"v1b": {
"legacy": True,
"msg": "please use 'other - v1b' for exact replacement, or 'fanlamp_pro' v1 / v2 / v3 if effectively using FanLamp Pro app",
},
},
"default_variant": "v3",
"default_forced_id": 0,
},
"lampsmart_pro": {
"variants": {
"v1": {
"class": FanLampEncoderV1,
"args": [ 0x81 ],
"max_forced_id": 0xFFFFFF,
"ble_param": [ 0x19, 0x03 ],
"header": [0x77, 0xF8],
},
# v2 is only used by LampSmart Pro - Soft Lighting
"v2": {
"class": FanLampEncoderV2,
"args": [ [0x10, 0x80, 0x00], 0x0100, False ],
"ble_param": [ 0x19, 0x03 ],
"header": [0xF0, 0x08],
},
"v3": {
"class": FanLampEncoderV2,
"args": [ [0x30, 0x80, 0x00], 0x0100, True ],
"ble_param": [ 0x19, 0x03 ],
"header": [0xF0, 0x08],
},
"v1a": {
"legacy": True,
"msg": "please use 'other - v1a' for exact replacement, or 'lampsmart_pro' v1 / v3 if effectively using LampSmart Pro app",
},
"v1b": {
"legacy": True,
"msg": "please use 'other - v1b' for exact replacement, or 'lampsmart_pro' v1 / v3 if effectively using LampSmart Pro app",
},
},
"default_variant": "v3",
"default_forced_id": 0,
},
"zhijia": {
"variants": {
"v0": {
"class": ZhijiaEncoderV0,
"args": [],
"max_forced_id": 0xFFFF,
"ble_param": [ 0x1A, 0xFF ],
"header": [ 0xF9, 0x08, 0x49 ],
},
"v1": {
"class": ZhijiaEncoderV1,
"args": [],
"max_forced_id": 0xFFFFFF,
"ble_param": [ 0x1A, 0xFF ],
"header": [ 0xF9, 0x08, 0x49 ],
},
"v2": {
"class": ZhijiaEncoderV2,
"args": [],
"max_forced_id": 0xFFFFFF,
"ble_param": [ 0x1A, 0xFF ],
"header": [ 0x22, 0x9D ],
},
},
"default_variant": "v2",
"default_forced_id": 0xC630B8,
},
"remote" : {
"variants": {
"v1": {
"class": FanLampEncoderV1,
"args": [ 0x83, False, True ],
"max_forced_id": 0xFFFFFF,
"ble_param": [ 0x00, 0xFF ],
"header":[0x56, 0x55, 0x18, 0x87, 0x52],
},
"v3": {
"class": FanLampEncoderV2,
"args": [ [0x10, 0x00, 0x56], 0x0400, True ],
"ble_param": [ 0x02, 0x16 ],
"header": [0xF0, 0x08],
},
},
"default_variant": "v3",
"default_forced_id": 0,
},
# legacy lampsmart_pro variants v1a / v1b / v2 / v3
# None of them are actually matching what FanLamp Pro / LampSmart Pro apps are generating
# Maybe generated by some remotes, kept here for backward compatibility, with some raw sample
"other" : {
"variants": {
"v1b": {
"class": FanLampEncoderV1,
"args": [ 0x81, True, True, 0x55 ],
"max_forced_id": 0xFFFFFF,
"ble_param": [ 0x02, 0x16 ],
"header": [0xF9, 0x08],
# 02.01.02.1B.03.F9.08.49.13.F0.69.25.4E.31.51.BA.32.08.0A.24.CB.3B.7C.71.DC.8B.B8.97.08.D0.4C (31)
},
"v1a": {
"class": FanLampEncoderV1,
"args": [ 0x81, True, True ],
"max_forced_id": 0xFFFFFF,
"ble_param": [ 0x02, 0x03 ],
"header": [0x77, 0xF8],
# 02.01.02.1B.03.77.F8.B6.5F.2B.5E.00.FC.31.51.50.CB.92.08.24.CB.BB.FC.14.C6.9E.B0.E9.EA.73.A4 (31)
},
"v2": {
"class": FanLampEncoderV2,
"args": [ [0x10, 0x80, 0x00], 0x0100, False ],
"ble_param": [ 0x19, 0x16 ],
"header": [0xF0, 0x08],
# 02.01.02.1B.16.F0.08.10.80.0B.9B.DA.CF.BE.B3.DD.56.3B.E9.1C.FC.27.A9.3A.A5.38.2D.3F.D4.6A.50 (31)
},
"v3": {
"class": FanLampEncoderV2,
"args": [ [0x10, 0x80, 0x00], 0x0100, True ],
"ble_param": [ 0x19, 0x16 ],
"header": [0xF0, 0x08],
# 02.01.02.1B.16.F0.08.10.80.33.BC.2E.B0.49.EA.58.76.C0.1D.99.5E.9C.D6.B8.0E.6E.14.2B.A5.30.A9 (31)
},
},
"default_variant": "v1b",
"default_forced_id": 0,
},
}

ENTITY_BASE_CONFIG_SCHEMA = cv.Schema(
{
cv.Required(CONF_BLE_ADV_CONTROLLER_ID): cv.use_id(BleAdvController),
}
)

CONTROLLER_BASE_CONFIG = cv.ENTITY_BASE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(BleAdvController),
cv.Optional(CONF_DURATION, default=200): cv.All(cv.positive_int, cv.Range(min=100, max=500)),
cv.Optional(CONF_BLE_ADV_MAX_DURATION, default=3000): cv.All(cv.positive_int, cv.Range(min=300, max=10000)),
cv.Optional(CONF_BLE_ADV_SEQ_DURATION, default=100): cv.All(cv.positive_int, cv.Range(min=0, max=150)),
cv.Optional(CONF_REVERSED, default=False): cv.boolean,
cv.Optional(CONF_BLE_ADV_SHOW_CONFIG, default=True): cv.boolean,
cv.Optional(CONF_INDEX, default=0): cv.All(cv.positive_int, cv.Range(min=0, max=255)),
}
)

def validate_legacy_variant(config):
encoding = config[CONF_BLE_ADV_ENCODING]
variant = config[CONF_VARIANT]
pv = BLE_ADV_ENCODERS[ encoding ]["variants"][ variant ]
if pv.get("legacy", False):
raise cv.Invalid("DEPRECATED '%s - %s', %s" % (encoding, variant, pv["msg"]))
return config

def validate_forced_id(config):
encoding = config[CONF_BLE_ADV_ENCODING]
variant = config[CONF_VARIANT]
forced_id = config[CONF_BLE_ADV_FORCED_ID]
params = BLE_ADV_ENCODERS[ encoding ]
max_forced_id = params["variants"][ variant ].get("max_forced_id", 0xFFFFFFFF)
if forced_id > max_forced_id :
raise cv.Invalid("Invalid 'forced_id' for %s - %s: %s. Maximum: 0x%X." % (encoding, variant, forced_id, max_forced_id))
return config

CONFIG_SCHEMA = cv.All(
cv.Any(
*[ CONTROLLER_BASE_CONFIG.extend(
{
cv.Required(CONF_BLE_ADV_ENCODING): cv.one_of(encoding),
cv.Optional(CONF_VARIANT, default=params["default_variant"]): cv.one_of(*params["variants"].keys()),
cv.Optional(CONF_BLE_ADV_FORCED_ID, default=params["default_forced_id"]): cv.hex_uint32_t,
}
) for encoding, params in BLE_ADV_ENCODERS.items() ]
),
validate_forced_id,
validate_legacy_variant,
cv.only_on([PLATFORM_ESP32]),
)

async def entity_base_code_gen(var, config):
await cg.register_parented(var, config[CONF_BLE_ADV_CONTROLLER_ID])
await cg.register_component(var, config)
await setup_entity(var, config)

class BleAdvRegistry:
handler = None
@classmethod
def get(cls):
if not cls.handler:
hdl_id = ID("ble_adv_static_handler", type=BleAdvHandler)
cls.handler = cg.new_Pvariable(hdl_id)
cg.add(cls.handler.set_component_source("ble_adv_handler"))
cg.add(cg.App.register_component(cls.handler))
for encoding, params in BLE_ADV_ENCODERS.items():
for variant, param_variant in params["variants"].items():
if "class" in param_variant:
enc_id = ID("enc_%s_%s" % (encoding, variant), type=param_variant["class"])
enc = cg.new_Pvariable(enc_id, encoding, variant, *param_variant["args"])
cg.add(enc.set_ble_param(*param_variant["ble_param"]))
cg.add(enc.set_header(param_variant["header"]))
cg.add(cls.handler.add_encoder(enc))
return cls.handler

async def to_code(config):
hdl = BleAdvRegistry.get()
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_setup_priority(300)) # start after Bluetooth
await cg.register_component(var, config)
await setup_entity(var, config)
cg.add(var.set_handler(hdl))
cg.add(var.set_encoding_and_variant(config[CONF_BLE_ADV_ENCODING], config[CONF_VARIANT]))
cg.add(var.set_min_tx_duration(config[CONF_DURATION], 100, 500, 10))
cg.add(var.set_max_tx_duration(config[CONF_BLE_ADV_MAX_DURATION]))
cg.add(var.set_seq_duration(config[CONF_BLE_ADV_SEQ_DURATION]))
cg.add(var.set_reversed(config[CONF_REVERSED]))
if CONF_BLE_ADV_FORCED_ID in config and config[CONF_BLE_ADV_FORCED_ID] > 0:
cg.add(var.set_forced_id(config[CONF_BLE_ADV_FORCED_ID]))
else:
cg.add(var.set_forced_id(config[CONF_ID].id))
cg.add(var.set_show_config(config[CONF_BLE_ADV_SHOW_CONFIG]))


Loading

0 comments on commit 43fe118

Please sign in to comment.