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 Yeelight Rotating Dimmer (YLKG07YL, YLKG08YL) support #32

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
137 changes: 137 additions & 0 deletions .clang-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
Language: Cpp
AccessModifierOffset: -1
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: DontAlign
AlignOperands: true
AlignTrailingComments: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: All
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: MultiLine
BinPackArguments: true
BinPackParameters: true
BraceWrapping:
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Attach
BreakBeforeInheritanceComma: false
BreakInheritanceList: BeforeColon
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeColon
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: true
ColumnLimit: 120
CommentPragmas: "^ IWYU pragma:"
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: true
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerAlignment: true
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
IncludeBlocks: Preserve
IncludeCategories:
- Regex: '^<ext/.*\.h>'
Priority: 2
- Regex: '^<.*\.h>'
Priority: 1
- Regex: "^<.*"
Priority: 2
- Regex: ".*"
Priority: 3
IncludeIsMainRegex: "([-_](test|unittest))?$"
IndentCaseLabels: true
IndentPPDirectives: None
IndentWidth: 2
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: false
MacroBlockBegin: ""
MacroBlockEnd: ""
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 1
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 2000
PointerAlignment: Right
RawStringFormats:
- Language: Cpp
Delimiters:
- cc
- CC
- cpp
- Cpp
- CPP
- "c++"
- "C++"
CanonicalDelimiter: ""
BasedOnStyle: google
- Language: TextProto
Delimiters:
- pb
- PB
- proto
- PROTO
EnclosingFunctions:
- EqualsProto
- EquivToProto
- PARSE_PARTIAL_TEXT_PROTO
- PARSE_TEST_PROTO
- PARSE_TEXT_PROTO
- ParseTextOrDie
- ParseTextProtoOrDie
CanonicalDelimiter: ""
BasedOnStyle: google
ReflowComments: true
SortIncludes: false
SortUsingDeclarations: false
SpaceAfterCStyleCast: true
SpaceAfterTemplateKeyword: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 2
SpacesInAngles: false
SpacesInContainerLiterals: false
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Auto
TabWidth: 2
UseTab: Never
7 changes: 6 additions & 1 deletion components/xiaomi_ble/xiaomi_ble.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ bool parse_xiaomi_value(uint8_t value_type, const uint8_t *data, uint8_t value_l
// remote control key code, 3 bytes
if ((value_type == 0x01) && (value_length == 3)) {
result.keycode = data[0];
result.is_long_press = data[2] == 2;
result.encoder_value = data[1];
result.action_type = data[2];
}
// motion detection, 1 byte, 8-bit unsigned integer
else if ((value_type == 0x03) && (value_length == 1)) {
Expand Down Expand Up @@ -214,6 +215,9 @@ optional<XiaomiParseResult> parse_xiaomi_header(const esp32_ble_tracker::Service
} else if ((raw[2] == 0x53) && (raw[3] == 0x01)) { // Yeelight Remote Control YLYK01YL
result.type = XiaomiParseResult::TYPE_YLYK01YL;
result.name = "YLYK01YL";
} else if ((raw[2] == 0xB6) && (raw[3] == 0x03)) { // Yeelight Wireless Smart Dimmer YLKG07YL/YLKG08YL
result.type = XiaomiParseResult::TYPE_YLKG07YL;
result.name = "YLKG07YL";
} else {
ESP_LOGVV(TAG, "parse_xiaomi_header(): unknown device, no magic bytes.");
return {};
Expand All @@ -222,6 +226,7 @@ optional<XiaomiParseResult> parse_xiaomi_header(const esp32_ble_tracker::Service
return result;
}

// Decrypt MiBeacon V4/V5 payload
bool decrypt_xiaomi_payload(std::vector<uint8_t> &raw, const uint8_t *bindkey, const uint64_t &address) {
if (!((raw.size() == 19) || ((raw.size() >= 22) && (raw.size() <= 24)))) {
ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): data packet has wrong size (%d)!", raw.size());
Expand Down
6 changes: 4 additions & 2 deletions components/xiaomi_ble/xiaomi_ble.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ struct XiaomiParseResult {
TYPE_MJYD02YLA,
TYPE_MHOC401,
TYPE_CGPR1,
TYPE_YLYK01YL
TYPE_YLYK01YL,
TYPE_YLKG07YL,
} type;
std::string name;
optional<int> keycode;
optional<int> encoder_value;
optional<int> action_type;
optional<float> temperature;
optional<float> humidity;
optional<float> moisture;
Expand All @@ -41,7 +44,6 @@ struct XiaomiParseResult {
optional<bool> is_active;
optional<bool> has_motion;
optional<bool> is_light;
optional<bool> is_long_press;
bool has_data; // 0x40
bool has_capability; // 0x20
bool has_encryption; // 0x08
Expand Down
132 changes: 132 additions & 0 deletions components/xiaomi_ylkg07yl/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, esp32_ble_tracker
from esphome import automation
from esphome.const import (
CONF_MAC_ADDRESS,
CONF_BINDKEY,
UNIT_EMPTY,
ICON_EMPTY,
DEVICE_CLASS_EMPTY,
CONF_ID,
CONF_TRIGGER_ID,
)

AUTO_LOAD = ["xiaomi_ble", "sensor"]
CODEOWNERS = ["@syssi"]
DEPENDENCIES = ["esp32_ble_tracker"]
MULTI_CONF = True

CONF_KEYCODE = "keycode"
CONF_ENCODER_VALUE = "encoder_value"
CONF_ACTION_TYPE = "action_type"

SENSORS = [
CONF_KEYCODE,
CONF_ENCODER_VALUE,
CONF_ACTION_TYPE,
]

CONF_ON_PRESS = "on_press"
CONF_ON_PRESS_AND_ROTATE = "on_press_and_rotate"
CONF_ON_ROTATE = "on_rotate"

ON_PRESS_ACTIONS = [
CONF_ON_PRESS,
CONF_ON_PRESS_AND_ROTATE,
CONF_ON_ROTATE,
]

xiaomi_ylkg07yl_ns = cg.esphome_ns.namespace("xiaomi_ylkg07yl")
XiaomiYLKG07YL = xiaomi_ylkg07yl_ns.class_(
"XiaomiYLKG07YL", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
)

OnPressTrigger = xiaomi_ylkg07yl_ns.class_(
"OnPressTrigger", automation.Trigger.template()
)

OnPressAndRotateTrigger = xiaomi_ylkg07yl_ns.class_(
"OnPressAndRotateTrigger", automation.Trigger.template()
)

OnRotateTrigger = xiaomi_ylkg07yl_ns.class_(
"OnRotateTrigger", automation.Trigger.template()
)


def validate_short_bind_key(value):
value = cv.string_strict(value)
parts = [value[i : i + 2] for i in range(0, len(value), 2)]
if len(parts) != 12:
raise cv.Invalid("Bind key must consist of 12 hexadecimal numbers")
parts_int = []
if any(len(part) != 2 for part in parts):
raise cv.Invalid("Bind key must be format XX")
for part in parts:
try:
parts_int.append(int(part, 16))
except ValueError:
# pylint: disable=raise-missing-from
raise cv.Invalid("Bind key must be hex values from 00 to FF")

return "".join(f"{part:02X}" for part in parts_int)


CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(XiaomiYLKG07YL),
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
cv.Required(CONF_BINDKEY): validate_short_bind_key,
cv.Optional(CONF_KEYCODE): sensor.sensor_schema(
UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY
),
cv.Optional(CONF_ENCODER_VALUE): sensor.sensor_schema(
UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY
),
cv.Optional(CONF_ACTION_TYPE): sensor.sensor_schema(
UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY
),
cv.Optional(CONF_ON_PRESS): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnPressTrigger),
}
),
cv.Optional(CONF_ON_PRESS_AND_ROTATE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
OnPressAndRotateTrigger
),
}
),
cv.Optional(CONF_ON_ROTATE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnRotateTrigger),
}
),
}
)
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)


async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await esp32_ble_tracker.register_ble_device(var, config)

cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
cg.add(var.set_bindkey(config[CONF_BINDKEY]))

for key in SENSORS:
if key in config:
conf = config[key]
sens = await sensor.new_sensor(conf)
cg.add(getattr(var, f"set_{key}_sensor")(sens))

for action in ON_PRESS_ACTIONS:
for conf in config.get(action, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
Loading