Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new ble_adv_controller component integrating the handling of fan, lights, pairing button and new variants #17

Merged
merged 22 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d702faf
Add new ble_adv_component integrating the handling of fan, lights, pa…
NicoIIT Jul 19, 2024
0239dec
Added reversed for ZhiJia. Reviewed Fan and Light param setup algo. R…
NicoIIT Jul 22, 2024
c1341a0
Review UUID computation
NicoIIT Jul 22, 2024
96775e1
Correct Warning during compilation, add Fan direction for fanlamp pro v1
NicoIIT Jul 24, 2024
194c128
Add handling of secondary light for FanLamp, remove index option but …
NicoIIT Jul 24, 2024
4891697
Updated doc to add technical limitations and more details first steps
NicoIIT Jul 24, 2024
6bbbc4b
Add Secondary light control for ZhiJia
NicoIIT Jul 24, 2024
a8d91ba
Workaround for flickering issue
NicoIIT Jul 25, 2024
a624b7d
Add Fan oscillation
NicoIIT Jul 29, 2024
494c903
Full review of the advertising process introducing sequencing, dynami…
NicoIIT Jul 28, 2024
e4a3c07
Add Dynamic configuration for Duration. Update doc.
NicoIIT Jul 29, 2024
8bac005
Correct max value for ZhiJia brightness and color temperature, code c…
NicoIIT Jul 30, 2024
dce1407
Revert to raw encoding for fanlamp_pro
NicoIIT Aug 1, 2024
dd2b3c8
Correct compilation issues on C3 board and esp-idf framework
NicoIIT Aug 12, 2024
d3b657b
restore_mode support for Fan
NicoIIT Aug 13, 2024
d714396
Fix Zhi Jia Light flickering issue
NicoIIT Aug 1, 2024
3bf45b7
add dynamic configuration for min_brightness
NicoIIT Aug 13, 2024
9084fa0
Add decoder, re align encodings with real apps, refactor encoders to …
NicoIIT Aug 16, 2024
b1cdd82
Remove legacy components, update doc
NicoIIT Aug 20, 2024
d609ece
Force direction / oscillation refresh on fan startup
NicoIIT Aug 20, 2024
b4241b5
Adding official support for more Apps
NicoIIT Aug 20, 2024
9537972
fix forced oscillation / direction double command on speed change
NicoIIT Aug 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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