Browse Source

Applied ESPHome-style code styling.

pull/20/head
Maurice Makaay 3 years ago
parent
commit
c90d9677f8
21 changed files with 1275 additions and 1051 deletions
  1. +137
    -0
      .clang-format
  2. +127
    -0
      .clang-tidy
  3. +37
    -33
      binary_sensor/touch_binary_sensor.h
  4. +4
    -4
      common.h
  5. +171
    -164
      front_panel_hal.h
  6. +46
    -46
      light/automation.h
  7. +30
    -31
      light/color_instant_handler.h
  8. +34
    -35
      light/color_night_light.h
  9. +15
    -16
      light/color_off.h
  10. +119
    -124
      light/color_rgb_light.h
  11. +103
    -110
      light/color_transition_handler.h
  12. +53
    -54
      light/color_white_light.h
  13. +30
    -33
      light/gpio_outputs.h
  14. +9
    -9
      light/light_modes.h
  15. +75
    -82
      light/light_output.h
  16. +14
    -15
      light/light_state.h
  17. +161
    -181
      light/presets.h
  18. +38
    -38
      light_hal.h
  19. +11
    -13
      output/output.h
  20. +43
    -45
      sensor/slider_sensor.h
  21. +18
    -18
      text_sensor/light_mode_text_sensor.h

+ 137
- 0
.clang-format View File

@ -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

+ 127
- 0
.clang-tidy View File

@ -0,0 +1,127 @@
---
Checks: >-
*,
-abseil-*,
-android-*,
-boost-*,
-bugprone-macro-parentheses,
-cert-dcl50-cpp,
-cert-err58-cpp,
-clang-analyzer-core.CallAndMessage,
-clang-analyzer-osx.*,
-clang-analyzer-security.*,
-cppcoreguidelines-avoid-goto,
-cppcoreguidelines-c-copy-assignment-signature,
-cppcoreguidelines-owning-memory,
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
-cppcoreguidelines-pro-bounds-constant-array-index,
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
-cppcoreguidelines-pro-type-const-cast,
-cppcoreguidelines-pro-type-cstyle-cast,
-cppcoreguidelines-pro-type-member-init,
-cppcoreguidelines-pro-type-reinterpret-cast,
-cppcoreguidelines-pro-type-static-cast-downcast,
-cppcoreguidelines-pro-type-union-access,
-cppcoreguidelines-pro-type-vararg,
-cppcoreguidelines-special-member-functions,
-fuchsia-*,
-fuchsia-default-arguments,
-fuchsia-multiple-inheritance,
-fuchsia-overloaded-operator,
-fuchsia-statically-constructed-objects,
-google-build-using-namespace,
-google-explicit-constructor,
-google-readability-braces-around-statements,
-google-readability-casting,
-google-readability-todo,
-google-runtime-int,
-google-runtime-references,
-hicpp-*,
-llvm-header-guard,
-llvm-include-order,
-misc-unconventional-assign-operator,
-misc-unused-parameters,
-modernize-deprecated-headers,
-modernize-pass-by-value,
-modernize-pass-by-value,
-modernize-return-braced-init-list,
-modernize-use-auto,
-modernize-use-default-member-init,
-modernize-use-equals-default,
-mpi-*,
-objc-*,
-performance-unnecessary-value-param,
-readability-braces-around-statements,
-readability-else-after-return,
-readability-implicit-bool-conversion,
-readability-named-parameter,
-readability-redundant-member-init,
-warnings-as-errors,
-zircon-*
WarningsAsErrors: '*'
HeaderFilterRegex: '^.*/src/esphome/.*'
AnalyzeTemporaryDtors: false
FormatStyle: google
CheckOptions:
- key: google-readability-braces-around-statements.ShortStatementLines
value: '1'
- key: google-readability-function-size.StatementThreshold
value: '800'
- key: google-readability-namespace-comments.ShortNamespaceLines
value: '10'
- key: google-readability-namespace-comments.SpacesBeforeComments
value: '2'
- key: modernize-loop-convert.MaxCopySize
value: '16'
- key: modernize-loop-convert.MinConfidence
value: reasonable
- key: modernize-loop-convert.NamingStyle
value: CamelCase
- key: modernize-pass-by-value.IncludeStyle
value: llvm
- key: modernize-replace-auto-ptr.IncludeStyle
value: llvm
- key: modernize-use-nullptr.NullMacros
value: 'NULL'
- key: readability-identifier-naming.LocalVariableCase
value: 'lower_case'
- key: readability-identifier-naming.ClassCase
value: 'CamelCase'
- key: readability-identifier-naming.StructCase
value: 'CamelCase'
- key: readability-identifier-naming.EnumCase
value: 'CamelCase'
- key: readability-identifier-naming.EnumConstantCase
value: 'UPPER_CASE'
- key: readability-identifier-naming.StaticConstantCase
value: 'UPPER_CASE'
- key: readability-identifier-naming.StaticVariableCase
value: 'UPPER_CASE'
- key: readability-identifier-naming.GlobalConstantCase
value: 'UPPER_CASE'
- key: readability-identifier-naming.ParameterCase
value: 'lower_case'
- key: readability-identifier-naming.PrivateMemberPrefix
value: 'NO_PRIVATE_MEMBERS_ALWAYS_USE_PROTECTED'
- key: readability-identifier-naming.PrivateMethodPrefix
value: 'NO_PRIVATE_METHODS_ALWAYS_USE_PROTECTED'
- key: readability-identifier-naming.ClassMemberCase
value: 'lower_case'
- key: readability-identifier-naming.ClassMemberCase
value: 'lower_case'
- key: readability-identifier-naming.ProtectedMemberCase
value: 'lower_case'
- key: readability-identifier-naming.ProtectedMemberSuffix
value: '_'
- key: readability-identifier-naming.FunctionCase
value: 'lower_case'
- key: readability-identifier-naming.ClassMethodCase
value: 'lower_case'
- key: readability-identifier-naming.ProtectedMethodCase
value: 'lower_case'
- key: readability-identifier-naming.ProtectedMethodSuffix
value: '_'
- key: readability-identifier-naming.VirtualMethodCase
value: 'lower_case'
- key: readability-identifier-naming.VirtualMethodSuffix
value: ''

+ 37
- 33
binary_sensor/touch_binary_sensor.h View File

@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "../common.h" #include "../common.h"
#include "../front_panel_hal.h" #include "../front_panel_hal.h"
@ -13,40 +13,44 @@ namespace bslamp2 {
* and touch slider on the front panel of the Xiaomi Mijia Bedside Lamp 2. * and touch slider on the front panel of the Xiaomi Mijia Bedside Lamp 2.
*/ */
class XiaomiBslamp2TouchBinarySensor : public binary_sensor::BinarySensor, public Component { class XiaomiBslamp2TouchBinarySensor : public binary_sensor::BinarySensor, public Component {
public:
void set_parent(FrontPanelHAL *front_panel) {
front_panel_ = front_panel;
}
public:
void set_parent(FrontPanelHAL *front_panel) { front_panel_ = front_panel; }
void set_for(int part) {
for_ = part;
}
void set_for(int part) { for_ = part; }
void setup() {
front_panel_->add_on_event_callback(
[this](EVENT ev) {
auto part_in_event = ev & FLAG_PART_MASK;
if (for_ == 0 || part_in_event == for_) {
auto new_state = (ev & FLAG_TYPE_MASK) == FLAG_TYPE_TOUCH;
this->publish_state(new_state);
}
}
);
}
void setup() {
front_panel_->add_on_event_callback([this](EVENT ev) {
auto part_in_event = ev & FLAG_PART_MASK;
if (for_ == 0 || part_in_event == for_) {
auto new_state = (ev & FLAG_TYPE_MASK) == FLAG_TYPE_TOUCH;
this->publish_state(new_state);
}
});
}
void dump_config() {
ESP_LOGCONFIG(TAG, "Front panel binary_sensor:");
ESP_LOGCONFIG(TAG, " For: %s",
for_ & FLAG_PART_POWER ? "power button" :
for_ & FLAG_PART_COLOR ? "color button" :
for_ & FLAG_PART_SLIDER ? "slider" : "ERR");
}
void dump_config() {
ESP_LOGCONFIG(TAG, "Front panel binary_sensor:");
ESP_LOGCONFIG(TAG, " For: %s", format_part());
}
protected:
FrontPanelHAL *front_panel_;
EVENT for_ = 0;
protected:
FrontPanelHAL *front_panel_;
EVENT for_ = 0;
const char *format_part() {
switch (for_) {
case FLAG_PART_POWER:
return "power button";
case FLAG_PART_COLOR:
return "color button";
case FLAG_PART_SLIDER:
return "slider";
default:
return "ERR";
}
}
}; };
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 4
- 4
common.h View File

@ -1,4 +1,4 @@
#pragma once
#pragma once
namespace esphome { namespace esphome {
namespace xiaomi { namespace xiaomi {
@ -7,6 +7,6 @@ namespace bslamp2 {
// Used for logging purposes. // Used for logging purposes.
static const char *TAG = "xiaomi_bslamp2"; static const char *TAG = "xiaomi_bslamp2";
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 171
- 164
front_panel_hal.h View File

@ -1,10 +1,10 @@
#pragma once
#pragma once
#include <array>
#include "common.h" #include "common.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/esphal.h" #include "esphome/core/esphal.h"
#include "esphome/components/i2c/i2c.h"
#include <array>
namespace esphome { namespace esphome {
namespace xiaomi { namespace xiaomi {
@ -13,20 +13,22 @@ namespace bslamp2 {
static const uint8_t MSG_LEN = 7; static const uint8_t MSG_LEN = 7;
using MSG = uint8_t[7]; using MSG = uint8_t[7];
// clang-format off
// The commands that are supported by the front panel component. // The commands that are supported by the front panel component.
static const MSG READY_FOR_EV = { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 };
static const MSG TURN_ON = { 0x02, 0x03, 0x5E, 0x00, 0x64, 0x00, 0x00 };
static const MSG TURN_OFF = { 0x02, 0x03, 0x0C, 0x00, 0x64, 0x00, 0x00 };
static const MSG SET_LEVEL_1 = { 0x02, 0x03, 0x5E, 0x00, 0x64, 0x00, 0x00 };
static const MSG SET_LEVEL_2 = { 0x02, 0x03, 0x5F, 0x00, 0x64, 0x00, 0x00 };
static const MSG SET_LEVEL_3 = { 0x02, 0x03, 0x5F, 0x80, 0x64, 0x00, 0x00 };
static const MSG SET_LEVEL_4 = { 0x02, 0x03, 0x5F, 0xC0, 0x64, 0x00, 0x00 };
static const MSG SET_LEVEL_5 = { 0x02, 0x03, 0x5F, 0xE0, 0x64, 0x00, 0x00 };
static const MSG SET_LEVEL_6 = { 0x02, 0x03, 0x5F, 0xF0, 0x64, 0x00, 0x00 };
static const MSG SET_LEVEL_7 = { 0x02, 0x03, 0x5F, 0xF8, 0x64, 0x00, 0x00 };
static const MSG SET_LEVEL_8 = { 0x02, 0x03, 0x5F, 0xFC, 0x64, 0x00, 0x00 };
static const MSG SET_LEVEL_9 = { 0x02, 0x03, 0x5F, 0xFE, 0x64, 0x00, 0x00 };
static const MSG SET_LEVEL_10 = { 0x02, 0x03, 0x5F, 0xFF, 0x64, 0x00, 0x00 };
static const MSG READY_FOR_EV = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01};
static const MSG TURN_ON = {0x02, 0x03, 0x5E, 0x00, 0x64, 0x00, 0x00};
static const MSG TURN_OFF = {0x02, 0x03, 0x0C, 0x00, 0x64, 0x00, 0x00};
static const MSG SET_LEVEL_1 = {0x02, 0x03, 0x5E, 0x00, 0x64, 0x00, 0x00};
static const MSG SET_LEVEL_2 = {0x02, 0x03, 0x5F, 0x00, 0x64, 0x00, 0x00};
static const MSG SET_LEVEL_3 = {0x02, 0x03, 0x5F, 0x80, 0x64, 0x00, 0x00};
static const MSG SET_LEVEL_4 = {0x02, 0x03, 0x5F, 0xC0, 0x64, 0x00, 0x00};
static const MSG SET_LEVEL_5 = {0x02, 0x03, 0x5F, 0xE0, 0x64, 0x00, 0x00};
static const MSG SET_LEVEL_6 = {0x02, 0x03, 0x5F, 0xF0, 0x64, 0x00, 0x00};
static const MSG SET_LEVEL_7 = {0x02, 0x03, 0x5F, 0xF8, 0x64, 0x00, 0x00};
static const MSG SET_LEVEL_8 = {0x02, 0x03, 0x5F, 0xFC, 0x64, 0x00, 0x00};
static const MSG SET_LEVEL_9 = {0x02, 0x03, 0x5F, 0xFE, 0x64, 0x00, 0x00};
static const MSG SET_LEVEL_10 = {0x02, 0x03, 0x5F, 0xFF, 0x64, 0x00, 0x00};
using EVENT = uint16_t; using EVENT = uint16_t;
@ -40,7 +42,7 @@ using EVENT = uint16_t;
// 2-4 part 000 part unknown // 2-4 part 000 part unknown
// 001 power button // 001 power button
// 010 color button // 010 color button
// 100 slider
// 100 slider
// 5-6 type 00 type unknown // 5-6 type 00 type unknown
// 01 touch // 01 touch
// 10 release // 10 release
@ -71,89 +73,100 @@ static const EVENT FLAG_LEVEL_SHIFT = 6;
static const EVENT FLAG_LEVEL_MASK = 0b11111000000; static const EVENT FLAG_LEVEL_MASK = 0b11111000000;
static const EVENT FLAG_LEVEL_UNKNOWN = 0b00000000000; static const EVENT FLAG_LEVEL_UNKNOWN = 0b00000000000;
// clang-format on
/** /**
* This class implements a parser that translates event byte codes from the * This class implements a parser that translates event byte codes from the
* Xiaomi Mijia Bedside Lamp 2 into usable events. * Xiaomi Mijia Bedside Lamp 2 into usable events.
*/ */
class FrontPanelEventParser { class FrontPanelEventParser {
public:
/**
* Parse the provided event byte code (7 bytes long).
* Returns a unique integer event code that describes the parsed event.
*/
EVENT parse(uint8_t *m) {
EVENT ev = FLAG_INIT;
// All events use the prefix [04:04:01:00].
if (m[0] != 0x04 || m[1] != 0x04 || m[2] != 0x01 || m[3] != 0x00) {
return error_(ev, m, "prefix is not 04:04:01:00");
}
public:
/**
* Parse the provided event byte code (7 bytes long).
* Returns a unique integer event code that describes the parsed event.
*/
EVENT parse(uint8_t *m) {
EVENT ev = FLAG_INIT;
// The next byte determines the part that is touched.
// All remaining bytes specify the event for that part.
switch (m[4]) {
case 0x01: // power button
case 0x02: // color button
ev |= (m[4] == 0x01 ? FLAG_PART_POWER : FLAG_PART_COLOR);
if (m[5] == 0x01 && m[6] == (0x02 + m[4])) {
ev |= FLAG_TYPE_TOUCH;
} else if (m[5] == 0x02 && m[6] == (0x03 + m[4])) {
ev |= FLAG_TYPE_RELEASE;
} else {
return error_(ev, m, "invalid event type for button");
}
break;
case 0x03: // slider touch
case 0x04: // slider release
ev |= FLAG_PART_SLIDER;
ev |= (m[4] == 0x03 ? FLAG_TYPE_TOUCH : FLAG_TYPE_RELEASE);
if ((m[6] - m[5] - m[4] - 0x01) != 0) {
return error_(ev, m, "invalid slider level crc");
} else if (m[5] > 0x16 || m[5] < 0x01) {
return error_(ev, m, "out of bounds slider value");
} else {
auto level = 0x17 - m[5];
ev |= (level << FLAG_LEVEL_SHIFT);
}
break;
default:
return error_(ev, m, "invalid part id");
return ev;
}
// All parsing rules passed. This event is valid.
ESP_LOGD(TAG, "Front panel I2C event parsed: code=%d", ev);
ev |= FLAG_OK;
// All events use the prefix [04:04:01:00].
if (m[0] != 0x04 || m[1] != 0x04 || m[2] != 0x01 || m[3] != 0x00) {
return error_(ev, m, "prefix is not 04:04:01:00");
}
// The next byte determines the part that is touched.
// All remaining bytes specify the event for that part.
switch (m[4]) {
case 0x01: // power button
case 0x02: // color button
ev |= (m[4] == 0x01 ? FLAG_PART_POWER : FLAG_PART_COLOR);
if (m[5] == 0x01 && m[6] == (0x02 + m[4]))
ev |= FLAG_TYPE_TOUCH;
else if (m[5] == 0x02 && m[6] == (0x03 + m[4]))
ev |= FLAG_TYPE_RELEASE;
else
return error_(ev, m, "invalid event type for button");
break;
case 0x03: // slider touch
case 0x04: // slider release
ev |= FLAG_PART_SLIDER;
ev |= (m[4] == 0x03 ? FLAG_TYPE_TOUCH : FLAG_TYPE_RELEASE);
if ((m[6] - m[5] - m[4] - 0x01) != 0)
return error_(ev, m, "invalid slider level crc");
else if (m[5] > 0x16 || m[5] < 0x01)
return error_(ev, m, "out of bounds slider value");
else {
auto level = 0x17 - m[5];
ev |= (level << FLAG_LEVEL_SHIFT);
}
break;
default:
return error_(ev, m, "invalid part id");
return ev; return ev;
} }
protected:
bool has_(EVENT ev, EVENT mask, EVENT flag) {
return (ev & mask) == flag;
}
// All parsing rules passed. This event is valid.
ESP_LOGD(TAG, "Front panel I2C event parsed: code=%d", ev);
ev |= FLAG_OK;
EVENT error_(EVENT ev, uint8_t *m, const char* msg) {
ESP_LOGE(TAG, "Front panel I2C event error:");
ESP_LOGE(TAG, " Error: %s", msg);
ESP_LOGE(TAG, " Event: [%02x:%02x:%02x:%02x:%02x:%02x:%02x]",
m[0], m[1], m[2], m[3], m[4], m[5], m[6]);
ESP_LOGE(TAG, " Parsed part: %s",
(has_(ev, FLAG_PART_MASK, FLAG_PART_POWER) ? "power button" :
has_(ev, FLAG_PART_MASK, FLAG_PART_COLOR) ? "color button" :
has_(ev, FLAG_PART_MASK, FLAG_PART_SLIDER) ? "slider" : "n/a"));
ESP_LOGE(TAG, " Parsed event type: %s",
(has_(ev, FLAG_TYPE_MASK, FLAG_TYPE_TOUCH) ? "touch" :
has_(ev, FLAG_TYPE_MASK, FLAG_TYPE_RELEASE) ? "release" : "n/a"));
if (has_(ev, FLAG_PART_MASK, FLAG_PART_SLIDER)) {
auto level = (ev & FLAG_LEVEL_MASK) >> FLAG_LEVEL_SHIFT;
if (level > 0) {
ESP_LOGE(TAG, " Parsed slider level: %d", level);
}
}
return ev;
}
return ev;
protected:
bool has_(EVENT ev, EVENT mask, EVENT flag) { return (ev & mask) == flag; }
EVENT error_(EVENT ev, uint8_t *m, const char *msg) {
ESP_LOGE(TAG, "Front panel I2C event error:");
ESP_LOGE(TAG, " Error: %s", msg);
ESP_LOGE(TAG, " Event: [%02x:%02x:%02x:%02x:%02x:%02x:%02x]", m[0], m[1], m[2], m[3], m[4], m[5], m[6]);
ESP_LOGE(TAG, " Parsed part: %s", format_part(ev));
ESP_LOGE(TAG, " Parsed event type: %s", format_event_type(ev));
if (has_(ev, FLAG_PART_MASK, FLAG_PART_SLIDER)) {
auto level = (ev & FLAG_LEVEL_MASK) >> FLAG_LEVEL_SHIFT;
if (level > 0) {
ESP_LOGE(TAG, " Parsed slider level: %d", level);
}
} }
return ev;
}
const char *format_part(EVENT ev) {
if (has_(ev, FLAG_PART_MASK, FLAG_PART_POWER))
return "power button";
if (has_(ev, FLAG_PART_MASK, FLAG_PART_COLOR))
return "color button";
if (has_(ev, FLAG_PART_MASK, FLAG_PART_SLIDER))
return "slider';
return "n/a";
}
const char *format_event_type(EVENT ev) {
if (has_(ev, FLAG_TYPE_MASK, FLAG_TYPE_TOUCH))
return "touch";
if (has_(ev, FLAG_TYPE_MASK, FLAG_TYPE_RELEASE))
return "release";
return "n/a";
}
}; };
/** /**
@ -164,86 +177,82 @@ protected:
* the actual buttons and slider components. * the actual buttons and slider components.
*/ */
class FrontPanelHAL : public Component, public i2c::I2CDevice { class FrontPanelHAL : public Component, public i2c::I2CDevice {
public:
FrontPanelEventParser event;
public:
FrontPanelEventParser event;
/**
* Set the GPIO pin that is used by the front panel to notify the ESP
* that a touch/release event can be read using I2C.
*/
void set_trigger_pin(GPIOPin *pin) { trigger_pin_ = pin; }
/**
* Set the GPIO pin that is used by the front panel to notify the ESP
* that a touch/release event can be read using I2C.
*/
void set_trigger_pin(GPIOPin *pin) { trigger_pin_ = pin; }
void add_on_event_callback(std::function<void(EVENT)> &&callback) {
event_callback_.add(std::move(callback));
}
void add_on_event_callback(std::function<void(EVENT)> &&callback) { event_callback_.add(std::move(callback)); }
void setup() {
ESP_LOGCONFIG(TAG, "Setting up I2C trigger pin interrupt...");
trigger_pin_->setup();
trigger_pin_->attach_interrupt(
FrontPanelHAL::isr, this, FALLING);
}
void setup() {
ESP_LOGCONFIG(TAG, "Setting up I2C trigger pin interrupt...");
trigger_pin_->setup();
trigger_pin_->attach_interrupt(FrontPanelHAL::isr, this, FALLING);
}
void dump_config() {
ESP_LOGCONFIG(TAG, "FrontPanelHAL:");
LOG_PIN(" I2C interrupt pin: ", trigger_pin_);
}
void dump_config() {
ESP_LOGCONFIG(TAG, "FrontPanelHAL:");
LOG_PIN(" I2C interrupt pin: ", trigger_pin_);
}
void loop() {
// Read and publish front panel events.
auto current_event_id = event_id_;
if (current_event_id != last_event_id_) {
last_event_id_ = current_event_id;
MSG message;
if (write_bytes_raw(READY_FOR_EV, MSG_LEN) &&
read_bytes_raw(message, MSG_LEN)) {
auto ev = event.parse(message);
if (ev & FLAG_OK) {
event_callback_.call(ev);
}
}
void loop() {
// Read and publish front panel events.
auto current_event_id = event_id_;
if (current_event_id != last_event_id_) {
last_event_id_ = current_event_id;
MSG message;
if (write_bytes_raw(READY_FOR_EV, MSG_LEN) && read_bytes_raw(message, MSG_LEN)) {
auto ev = event.parse(message);
if (ev & FLAG_OK) {
event_callback_.call(ev);
} }
}
} }
}
/**
* Sets the front panel illumination to the provided level (0.0 - 1.0).
*
* Level 0.0 means: turn off the front panel illumination.
* The other levels are translated to one of the available levels,
* represented by the level indicator (i.e. the illumination of the
* slider bar.)
*/
void set_light_level(float level) {
if (level == 0.0f)
write_bytes_raw(TURN_OFF, MSG_LEN);
else if (level < 0.15)
write_bytes_raw(SET_LEVEL_1, MSG_LEN);
else if (level < 0.25)
write_bytes_raw(SET_LEVEL_2, MSG_LEN);
else if (level < 0.35)
write_bytes_raw(SET_LEVEL_3, MSG_LEN);
else if (level < 0.45)
write_bytes_raw(SET_LEVEL_4, MSG_LEN);
else if (level < 0.55)
write_bytes_raw(SET_LEVEL_5, MSG_LEN);
else if (level < 0.65)
write_bytes_raw(SET_LEVEL_6, MSG_LEN);
else if (level < 0.75)
write_bytes_raw(SET_LEVEL_7, MSG_LEN);
else if (level < 0.85)
write_bytes_raw(SET_LEVEL_8, MSG_LEN);
else if (level < 0.95)
write_bytes_raw(SET_LEVEL_9, MSG_LEN);
else
write_bytes_raw(SET_LEVEL_10, MSG_LEN);
}
/**
* Sets the front panel illumination to the provided level (0.0 - 1.0).
*
* Level 0.0 means: turn off the front panel illumination.
* The other levels are translated to one of the available levels,
* represented by the level indicator (i.e. the illumination of the
* slider bar.)
*/
void set_light_level(float level) {
if (level == 0.0f)
write_bytes_raw(TURN_OFF, MSG_LEN);
else if (level < 0.15)
write_bytes_raw(SET_LEVEL_1, MSG_LEN);
else if (level < 0.25)
write_bytes_raw(SET_LEVEL_2, MSG_LEN);
else if (level < 0.35)
write_bytes_raw(SET_LEVEL_3, MSG_LEN);
else if (level < 0.45)
write_bytes_raw(SET_LEVEL_4, MSG_LEN);
else if (level < 0.55)
write_bytes_raw(SET_LEVEL_5, MSG_LEN);
else if (level < 0.65)
write_bytes_raw(SET_LEVEL_6, MSG_LEN);
else if (level < 0.75)
write_bytes_raw(SET_LEVEL_7, MSG_LEN);
else if (level < 0.85)
write_bytes_raw(SET_LEVEL_8, MSG_LEN);
else if (level < 0.95)
write_bytes_raw(SET_LEVEL_9, MSG_LEN);
else
write_bytes_raw(SET_LEVEL_10, MSG_LEN);
}
protected:
GPIOPin *trigger_pin_;
static void isr(FrontPanelHAL *store);
volatile int event_id_ = 0;
int last_event_id_ = 0;
CallbackManager<void(EVENT)> event_callback_{};
protected:
GPIOPin *trigger_pin_;
static void isr(FrontPanelHAL *store);
volatile int event_id_ = 0;
int last_event_id_ = 0;
CallbackManager<void(EVENT)> event_callback_{};
}; };
/** /**
@ -254,10 +263,8 @@ protected:
* is increment a simple event id counter. The main loop of the component * is increment a simple event id counter. The main loop of the component
* will take care of actually reading and processing the event. * will take care of actually reading and processing the event.
*/ */
void ICACHE_RAM_ATTR HOT FrontPanelHAL::isr(FrontPanelHAL *store) {
store->event_id_++;
}
void ICACHE_RAM_ATTR HOT FrontPanelHAL::isr(FrontPanelHAL *store) { store->event_id_++; }
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 46
- 46
light/automation.h View File

@ -1,63 +1,63 @@
#pragma once #pragma once
#include <cmath>
#include "esphome/core/component.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "light_output.h" #include "light_output.h"
#include "presets.h" #include "presets.h"
#include <cmath>
namespace esphome { namespace esphome {
namespace xiaomi { namespace xiaomi {
namespace bslamp2 { namespace bslamp2 {
class BrightnessTrigger : public Trigger<float> { class BrightnessTrigger : public Trigger<float> {
public:
explicit BrightnessTrigger(XiaomiBslamp2LightOutput *parent) {
parent->add_on_state_callback([this](light::LightColorValues values) {
auto new_brightness = values.get_brightness();
if (values.get_state() == 0) {
new_brightness = 0.0f;
}
new_brightness = roundf(new_brightness * 100.0f) / 100.0f;
if (last_brightness_ != new_brightness) {
trigger(new_brightness);
last_brightness_ = new_brightness;
}
});
}
protected:
float last_brightness_ = -1.0f;
public:
explicit BrightnessTrigger(XiaomiBslamp2LightOutput *parent) {
parent->add_on_state_callback([this](light::LightColorValues values) {
auto new_brightness = values.get_brightness();
if (values.get_state() == 0)
new_brightness = 0.0f;
new_brightness = roundf(new_brightness * 100.0f) / 100.0f;
if (last_brightness_ != new_brightness) {
trigger(new_brightness);
last_brightness_ = new_brightness;
}
});
}
protected:
float last_brightness_ = -1.0f;
}; };
template<typename... Ts> class ActivatePresetAction : public Action<Ts...> { template<typename... Ts> class ActivatePresetAction : public Action<Ts...> {
public:
explicit ActivatePresetAction(PresetsContainer *presets) : presets_(presets) {}
TEMPLATABLE_VALUE(std::string, operation);
TEMPLATABLE_VALUE(std::string, group);
TEMPLATABLE_VALUE(std::string, preset);
void play(Ts... x) override {
auto operation = this->operation_.value(x...);
if (operation == "next_group") {
presets_->activate_next_group();
} else if (operation == "next_preset") {
presets_->activate_next_preset();
} else if (operation == "activate_group") {
auto group = this->group_.value(x...);
presets_->activate_group(group);
} else if (operation == "activate_preset") {
auto group = this->group_.value(x...);
auto preset = this->preset_.value(x...);
presets_->activate_preset(group, preset);
}
public:
explicit ActivatePresetAction(PresetsContainer *presets) : presets_(presets) {}
TEMPLATABLE_VALUE(std::string, operation);
TEMPLATABLE_VALUE(std::string, group);
TEMPLATABLE_VALUE(std::string, preset);
void play(Ts... x) override {
auto operation = this->operation_.value(x...);
if (operation == "next_group") {
presets_->activate_next_group();
} else if (operation == "next_preset") {
presets_->activate_next_preset();
} else if (operation == "activate_group") {
auto group = this->group_.value(x...);
presets_->activate_group(group);
} else if (operation == "activate_preset") {
auto group = this->group_.value(x...);
auto preset = this->preset_.value(x...);
presets_->activate_preset(group, preset);
} }
}
protected:
PresetsContainer *presets_;
protected:
PresetsContainer *presets_;
}; };
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 30
- 31
light/color_instant_handler.h View File

@ -4,17 +4,17 @@
#include <stdexcept> #include <stdexcept>
#include "../common.h" #include "../common.h"
#include "gpio_outputs.h"
#include "color_off.h"
#include "color_night_light.h" #include "color_night_light.h"
#include "color_white_light.h"
#include "color_off.h"
#include "color_rgb_light.h" #include "color_rgb_light.h"
#include "color_white_light.h"
#include "gpio_outputs.h"
namespace esphome { namespace esphome {
namespace xiaomi { namespace xiaomi {
namespace bslamp2 { namespace bslamp2 {
/**
/**
* This class translates LightColorValues into GPIO duty cycles that * This class translates LightColorValues into GPIO duty cycles that
* can be used for representing a requested light color on the * can be used for representing a requested light color on the
* physical device. * physical device.
@ -27,34 +27,33 @@ namespace bslamp2 {
* - RGB light: based on RGB values + brightness * - RGB light: based on RGB values + brightness
*/ */
class ColorInstantHandler : public GPIOOutputs { class ColorInstantHandler : public GPIOOutputs {
public:
bool set_light_color_values(light::LightColorValues v) {
// The actual implementation of the various light modes is in
// separated targeted classes. These classes are called here
// in a chain of command-like pattern, to let the first one
// that can handle the light settings do the honours.
if (off_light_->set_light_color_values(v))
off_light_->copy_to(this);
else if (night_light_->set_light_color_values(v))
night_light_->copy_to(this);
else if (white_light_->set_light_color_values(v))
white_light_->copy_to(this);
else if (rgb_light_->set_light_color_values(v))
rgb_light_->copy_to(this);
else
throw std::logic_error(
"None of the GPIOOutputs classes handles the requested light state");
public:
bool set_light_color_values(light::LightColorValues v) {
// The actual implementation of the various light modes is in
// separated targeted classes. These classes are called here
// in a chain of command-like pattern, to let the first one
// that can handle the light settings do the honours.
if (off_light_->set_light_color_values(v))
off_light_->copy_to(this);
else if (night_light_->set_light_color_values(v))
night_light_->copy_to(this);
else if (white_light_->set_light_color_values(v))
white_light_->copy_to(this);
else if (rgb_light_->set_light_color_values(v))
rgb_light_->copy_to(this);
else
throw std::logic_error("None of the GPIOOutputs classes handles the requested light state");
return true;
}
return true;
}
protected:
GPIOOutputs *off_light_ = new ColorOff();
GPIOOutputs *rgb_light_ = new ColorRGBLight();
GPIOOutputs *white_light_ = new ColorWhiteLight();
GPIOOutputs *night_light_ = new ColorNightLight();
protected:
GPIOOutputs *off_light_ = new ColorOff();
GPIOOutputs *rgb_light_ = new ColorRGBLight();
GPIOOutputs *white_light_ = new ColorWhiteLight();
GPIOOutputs *night_light_ = new ColorNightLight();
}; };
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 34
- 35
light/color_night_light.h View File

@ -1,8 +1,8 @@
#pragma once #pragma once
#include "../common.h" #include "../common.h"
#include "light_modes.h"
#include "gpio_outputs.h" #include "gpio_outputs.h"
#include "light_modes.h"
namespace esphome { namespace esphome {
namespace xiaomi { namespace xiaomi {
@ -23,42 +23,41 @@ namespace bslamp2 {
* device's yaml configuration. * device's yaml configuration.
*/ */
class ColorNightLight : public GPIOOutputs { class ColorNightLight : public GPIOOutputs {
public:
bool set_light_color_values(light::LightColorValues v) {
light_mode = LIGHT_MODE_NIGHT;
public:
bool set_light_color_values(light::LightColorValues v) {
light_mode = LIGHT_MODE_NIGHT;
// Note: I do not check for a brightness at or below 0.01 (1%) here,
// because the lowest brightness setting from Home Assistant turns
// up as 0.011765 in here (which is 3/255 and not 1/100).
if (v.get_brightness() >= 0.012f) {
return false;
}
// Note: I do not check for a brightness at or below 0.01 (1%) here,
// because the lowest brightness setting from Home Assistant turns
// up as 0.011765 in here (which is 3/255 and not 1/100).
if (v.get_brightness() >= 0.012f)
return false;
// This night light mode is activated when white light is selected.
// Based on measurements using the original device firmware, so it
// matches the night light of the original firmware.
if (v.get_white() > 0) {
red = 0.968f;
green = 0.968f;
blue = 0.972f;
white = 0.0f;
}
// In RGB mode, the selected color is used to give the night light a
// specific color, instead of the default. This is a nice extra for
// this firmware, as the original firmware does not support it.
else {
red = esphome::lerp(v.get_red(), 0.9997f, 0.9680f);
green = esphome::lerp(v.get_green(), 0.9997f, 0.9680f);
auto blue_scale = (v.get_red() + v.get_green()) / 2.0f;
auto blue_max = esphome::lerp(blue_scale, 0.9640f, 0.9720f);
blue = esphome::lerp(v.get_blue(), 0.9997f, blue_max);
white = 0.0f;
}
return true;
// This night light mode is activated when white light is selected.
// Based on measurements using the original device firmware, so it
// matches the night light of the original firmware.
if (v.get_white() > 0) {
red = 0.968f;
green = 0.968f;
blue = 0.972f;
white = 0.0f;
}
// In RGB mode, the selected color is used to give the night light a
// specific color, instead of the default. This is a nice extra for
// this firmware, as the original firmware does not support it.
else {
red = esphome::lerp(v.get_red(), 0.9997f, 0.9680f);
green = esphome::lerp(v.get_green(), 0.9997f, 0.9680f);
auto blue_scale = (v.get_red() + v.get_green()) / 2.0f;
auto blue_max = esphome::lerp(blue_scale, 0.9640f, 0.9720f);
blue = esphome::lerp(v.get_blue(), 0.9997f, blue_max);
white = 0.0f;
} }
return true;
}
}; };
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 15
- 16
light/color_off.h View File

@ -1,8 +1,8 @@
#pragma once #pragma once
#include "../common.h" #include "../common.h"
#include "light_modes.h"
#include "gpio_outputs.h" #include "gpio_outputs.h"
#include "light_modes.h"
namespace esphome { namespace esphome {
namespace xiaomi { namespace xiaomi {
@ -12,23 +12,22 @@ namespace bslamp2 {
* This class can handle the GPIO outputs in case the light of turned off. * This class can handle the GPIO outputs in case the light of turned off.
*/ */
class ColorOff : public GPIOOutputs { class ColorOff : public GPIOOutputs {
public:
bool set_light_color_values(light::LightColorValues v) {
light_mode = LIGHT_MODE_OFF;
public:
bool set_light_color_values(light::LightColorValues v) {
light_mode = LIGHT_MODE_OFF;
if (v.get_state() != 0.0f && v.get_brightness() != 0.0f) {
return false;
}
if (v.get_state() != 0.0f && v.get_brightness() != 0.0f)
return false;
red = 1.0f;
green = 1.0f;
blue = 1.0f;
white = 0.0f;
red = 1.0f;
green = 1.0f;
blue = 1.0f;
white = 0.0f;
return true;
}
return true;
}
}; };
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 119
- 124
light/color_rgb_light.h View File

@ -4,22 +4,22 @@
#include <cmath> #include <cmath>
#include "../common.h" #include "../common.h"
#include "light_modes.h"
#include "gpio_outputs.h" #include "gpio_outputs.h"
#include "light_modes.h"
namespace esphome { namespace esphome {
namespace xiaomi { namespace xiaomi {
namespace bslamp2 { namespace bslamp2 {
struct RGB { struct RGB {
float red;
float green;
float blue;
float red;
float green;
float blue;
}; };
struct RGBPoint { struct RGBPoint {
RGB low;
RGB high;
RGB low;
RGB high;
}; };
using RGBRing = std::array<RGBPoint, 24>; using RGBRing = std::array<RGBPoint, 24>;
@ -27,8 +27,8 @@ using RGBRing = std::array<RGBPoint, 24>;
using RGBCircle = std::array<RGBRing, 7>; using RGBCircle = std::array<RGBRing, 7>;
/** /**
* The following table contains GPIO PWM duty cycles as used for driving the
* LEDs in the device in RGB mode.
* The following table contains GPIO PWM duty cycles as used for driving
* the LEDs in the device in RGB mode.
* *
* The base for this table are measurements against the original device * The base for this table are measurements against the original device
* firmware, using the RGB color circle as used in Home Assistant as the * firmware, using the RGB color circle as used in Home Assistant as the
@ -49,6 +49,7 @@ using RGBCircle = std::array<RGBRing, 7>;
* in-between brightnesses can be derived from these values by means of * in-between brightnesses can be derived from these values by means of
* linear interpolation. * linear interpolation.
*/ */
// clang-format off
static const RGBCircle rgb_circle_ {{ static const RGBCircle rgb_circle_ {{
// Ring 0, min value RGB component value = 0 // Ring 0, min value RGB component value = 0
{{ {{
@ -240,144 +241,138 @@ static const RGBCircle rgb_circle_ {{
{{ 0.9167, 0.8727, 0.9360 }, { 0.4406, 0.0000, 0.6330 }} // 345° [255,228,219] {{ 0.9167, 0.8727, 0.9360 }, { 0.4406, 0.0000, 0.6330 }} // 345° [255,228,219]
}} }}
}}; }};
// clang-format on
/** /**
* This class can handle the GPIO outputs for the RGB light mode, * This class can handle the GPIO outputs for the RGB light mode,
* based on RGB color values + brightness. * based on RGB color values + brightness.
*/ */
class ColorRGBLight : public GPIOOutputs { class ColorRGBLight : public GPIOOutputs {
public:
bool set_light_color_values(light::LightColorValues v) {
light_mode = LIGHT_MODE_RGB;
public:
bool set_light_color_values(light::LightColorValues v) {
light_mode = LIGHT_MODE_RGB;
if (v.get_white() > 0.0f) {
return false;
}
// Determine the ring level for the color. This is a value between 0
// and 7, determining in what ring of the RGB circle the requested
// color resides.
auto rgb_min = min(min(v.get_red(), v.get_green()), v.get_blue());
auto level = 7.0f * rgb_min;
if (v.get_white() > 0.0f) {
return false;
}
// While the default color circle in Home Assistant presents only a
// subset of colors, it is possible to request colors outside this
// subset as well. Therefore, the ring level might contain a
// fractional value instead of a plain integer. To accomodate for
// this, interpolation will be done to get the final outputs.
// Determine the ring level for the color. This is a value between 0
// and 7, determining in what ring of the RGB circle the requested
// color resides.
auto rgb_min = min(min(v.get_red(), v.get_green()), v.get_blue());
auto level = 7.0f * rgb_min;
// Determine duty cycle measurements for the outer ring.
auto level_a = floor(level);
set_duty_cycles_(
&rgbp_a_, level_a, v.get_red(), v.get_green(), v.get_blue(),
v.get_brightness(), &rgb_a_);
// While the default color circle in Home Assistant presents only a
// subset of colors, it is possible to request colors outside this
// subset as well. Therefore, the ring level might contain a
// fractional value instead of a plain integer. To accomodate for
// this, interpolation will be done to get the final outputs.
// Determine duty cycle measurements for the inner ring.
set_duty_cycles_(
&rgbp_b_, level_a, v.get_red(), v.get_green(), v.get_blue(),
v.get_brightness(), &rgb_b_);
// Determine duty cycle measurements for the outer ring.
auto level_a = floor(level);
set_duty_cycles_(&rgbp_a_, level_a, v.get_red(), v.get_green(), v.get_blue(), v.get_brightness(), &rgb_a_);
// Almost there! We now have the correct duty cycles for the
// two rings that we were looking at. In this last step, the
// two values are interpolated based on the ring level.
auto d = level - level_a;
red = esphome::lerp(d, rgb_a_.red, rgb_b_.red);
green = esphome::lerp(d, rgb_a_.green, rgb_b_.green);
blue = esphome::lerp(d, rgb_a_.blue, rgb_b_.blue);
// Determine duty cycle measurements for the inner ring.
set_duty_cycles_(&rgbp_b_, level_a, v.get_red(), v.get_green(), v.get_blue(), v.get_brightness(), &rgb_b_);
// The white output channel will always be 0 for RGB.
white = 0.0f;
// Almost there! We now have the correct duty cycles for the
// two rings that we were looking at. In this last step, the
// two values are interpolated based on the ring level.
auto d = level - level_a;
red = esphome::lerp(d, rgb_a_.red, rgb_b_.red);
green = esphome::lerp(d, rgb_a_.green, rgb_b_.green);
blue = esphome::lerp(d, rgb_a_.blue, rgb_b_.blue);
return true;
}
// The white output channel will always be 0 for RGB.
white = 0.0f;
RGBPoint rgbp_a_;
RGBPoint rgbp_b_;
RGB rgb_a_;
RGB rgb_b_;
return true;
}
void set_duty_cycles_(RGBPoint *p, int ring_level,
float r, float g, float b, float brightness, RGB *rgb) {
RGBPoint rgbp_a_;
RGBPoint rgbp_b_;
RGB rgb_a_;
RGB rgb_b_;
// Ring level 7 = white light center. The duty cycles for this level
// can be computed using a few basic functions.
if (ring_level == 7) {
rgb->red = 0.932101 - 0.383377 * brightness;
rgb->green = 0.883185 - 0.881623 * brightness;
rgb->blue = 0.94188 - 0.284498 * brightness;
return;
}
void set_duty_cycles_(RGBPoint *p, int ring_level, float r, float g, float b, float brightness, RGB *rgb) {
// Ring level 7 = white light center. The duty cycles for this level
// can be computed using a few basic functions.
if (ring_level == 7) {
rgb->red = 0.932101 - 0.383377 * brightness;
rgb->green = 0.883185 - 0.881623 * brightness;
rgb->blue = 0.94188 - 0.284498 * brightness;
return;
}
// Other ring levels are more complex. Start by retrieving the duty
// cycle measurement data for the ring at hand.
auto ring = rgb_circle_[ring_level];
// Other ring levels are more complex. Start by retrieving the duty
// cycle measurement data for the ring at hand.
auto ring = rgb_circle_[ring_level];
// Because we only have a subset of all colors in the RGB ring
// available in the configuration table, some interpolation will
// have to be done.
// First, compute the position on the ring for the requested RGB
// color. This is basically a hue representation of the requested
// color. It is expressed as a number of degrees around the ring,
// starting with red (at 0°).
auto pos = ring_pos_(r, g, b) / 15.0f;
// Because we only have a subset of all colors in the RGB ring
// available in the configuration table, some interpolation will
// have to be done.
// First, compute the position on the ring for the requested RGB
// color. This is basically a hue representation of the requested
// color. It is expressed as a number of degrees around the ring,
// starting with red (at 0°).
auto pos = ring_pos_(r, g, b) / 15.0f;
// Since there are 24 measurements for each ring, each measurement
// covers 360°/24 = 15°. Using that knowledge, the measurements to
// use for interpolation can be picked from the ring data.
auto pos_x = floor(pos);
auto x = ring[pos_x];
auto pos_y = ceil(pos);
auto y = ring[pos_y > 23 ? 0 : pos_y];
// Since there are 24 measurements for each ring, each measurement
// covers 360°/24 = 15°. Using that knowledge, the measurements to
// use for interpolation can be picked from the ring data.
auto pos_x = floor(pos);
auto x = ring[pos_x];
auto pos_y = ceil(pos);
auto y = ring[pos_y > 23 ? 0 : pos_y];
// Interpolate based on the ring position.
auto d = pos - pos_x;
p->low.red = esphome::lerp(d, x.low.red, y.low.red);
p->low.green = esphome::lerp(d, x.low.green, y.low.green);
p->low.blue = esphome::lerp(d, x.low.blue, y.low.blue);
p->high.red = esphome::lerp(d, x.high.red, y.high.red);
p->high.green = esphome::lerp(d, x.high.green, y.high.green);
p->high.blue = esphome::lerp(d, x.high.blue, y.high.blue);
// Interpolate based on the ring position.
auto d = pos - pos_x;
p->low.red = esphome::lerp(d, x.low.red, y.low.red);
p->low.green = esphome::lerp(d, x.low.green, y.low.green);
p->low.blue = esphome::lerp(d, x.low.blue, y.low.blue);
p->high.red = esphome::lerp(d, x.high.red, y.high.red);
p->high.green = esphome::lerp(d, x.high.green, y.high.green);
p->high.blue = esphome::lerp(d, x.high.blue, y.high.blue);
// Interpolate based on brightness level.
apply_brightness_(p, brightness, rgb);
}
// Interpolate based on brightness level.
apply_brightness_(p, brightness, rgb);
}
protected:
/**
* Returns the position on an RGB ring in degrees (0 - 359).
*/
float ring_pos_(float red, float green, float blue) {
auto rgb_min = min(min(red, green), blue);
auto rgb_max = max(max(red, green), blue);
auto delta = rgb_max - rgb_min;
float pos;
if (delta == 0.0f)
pos = 0.0f;
else if (red == rgb_max)
pos = 60.0f * fmod((green - blue) / delta, 6);
else if (green == rgb_max)
pos = 60.0f * ((blue - red) / delta + 2.0f);
else
pos = 60.0f * ((red - green) / delta + 4.0f);
if (pos < 0)
pos = pos + 360;
return pos;
}
protected:
/**
* Returns the position on an RGB ring in degrees (0 - 359).
*/
float ring_pos_(float red, float green, float blue) {
auto rgb_min = min(min(red, green), blue);
auto rgb_max = max(max(red, green), blue);
auto delta = rgb_max - rgb_min;
float pos;
if (delta == 0.0f)
pos = 0.0f;
else if (red == rgb_max)
pos = 60.0f * fmod((green - blue) / delta, 6);
else if (green == rgb_max)
pos = 60.0f * ((blue - red) / delta + 2.0f);
else
pos = 60.0f * ((red - green) / delta + 4.0f);
if (pos < 0)
pos = pos + 360;
return pos;
}
/**
* Apply brightness interpolation to the duty cycle measurements. We
* have the low (0.01) and high (1.00) brightness measurements in the
* data. Brightness can be applied by means of linear interpolation.
*/
void apply_brightness_(RGBPoint *p, float brightness, RGB *rgb) {
auto d = brightness - 0.01f;
rgb->red = esphome::lerp(d, p->low.red, p->high.red);
rgb->green = esphome::lerp(d, p->low.green, p->high.green);
rgb->blue = esphome::lerp(d, p->low.blue, p->high.blue);
}
/**
* Apply brightness interpolation to the duty cycle measurements. We
* have the low (0.01) and high (1.00) brightness measurements in the
* data. Brightness can be applied by means of linear interpolation.
*/
void apply_brightness_(RGBPoint *p, float brightness, RGB *rgb) {
auto d = brightness - 0.01f;
rgb->red = esphome::lerp(d, p->low.red, p->high.red);
rgb->green = esphome::lerp(d, p->low.green, p->high.green);
rgb->blue = esphome::lerp(d, p->low.blue, p->high.blue);
}
}; };
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 103
- 110
light/color_transition_handler.h View File

@ -1,8 +1,8 @@
#pragma once #pragma once
#include "../common.h" #include "../common.h"
#include "gpio_outputs.h"
#include "color_instant_handler.h" #include "color_instant_handler.h"
#include "gpio_outputs.h"
namespace esphome { namespace esphome {
namespace xiaomi { namespace xiaomi {
@ -17,142 +17,135 @@ namespace bslamp2 {
* check whether or not a light color transition is in progress. * check whether or not a light color transition is in progress.
*/ */
class LightStateTransformerInspector { class LightStateTransformerInspector {
public:
virtual bool is_active() = 0;
virtual bool is_transition() = 0;
virtual light::LightColorValues get_end_values() = 0;
virtual float get_progress() = 0;
public:
virtual bool is_active() = 0;
virtual bool is_transition() = 0;
virtual light::LightColorValues get_end_values() = 0;
virtual float get_progress() = 0;
}; };
/** /**
* This class is used to handle specific light color transition requirements * This class is used to handle specific light color transition requirements
* for the device. * for the device.
*
*
* When using the default ESPHome logic, transitioning is done by * When using the default ESPHome logic, transitioning is done by
* transitioning all light properties linearly from the original values to * transitioning all light properties linearly from the original values to
* the new values, and letting the light output object translate these * the new values, and letting the light output object translate these
* properties into light outputs on every step of the way. While this does * properties into light outputs on every step of the way. While this does
* work, it does not work nicely. * work, it does not work nicely.
*
*
* For example, when transitioning from warm to cold white light, the color * For example, when transitioning from warm to cold white light, the color
* temperature would be transitioned from the old value to the new value. * temperature would be transitioned from the old value to the new value.
* While doing so, the transition hits the middle white light setting, which * While doing so, the transition hits the middle white light setting, which
* shows up as a bright flash in the middle of the transition. The original * shows up as a bright flash in the middle of the transition. The original
* firmware however, shows a smooth transition from warm to cold white * firmware however, shows a smooth transition from warm to cold white
* light, without any flash. * light, without any flash.
*
*
* This class handles transitions by not varying the light properties over * This class handles transitions by not varying the light properties over
* time, but by transitioning the LEDC duty cycle output levels over time. * time, but by transitioning the LEDC duty cycle output levels over time.
* This matches the behavior of the original firmware. * This matches the behavior of the original firmware.
*/ */
class ColorTransitionHandler : public GPIOOutputs { class ColorTransitionHandler : public GPIOOutputs {
public:
ColorTransitionHandler(LightStateTransformerInspector *inspector) : transformer_(inspector) {}
light::LightColorValues get_end_values() {
return end_light_values_;
public:
ColorTransitionHandler(LightStateTransformerInspector *inspector) : transformer_(inspector) {}
light::LightColorValues get_end_values() { return end_light_values_; }
bool set_light_color_values(light::LightColorValues values) {
if (!light_state_has_active_transition_()) {
// Remember the last active light color values. When a transition
// is detected, we'll use these as the starting point. It is not
// possible to use the current values at that point, because the
// transition is already in progress by the time the transition
// is detected.
start_light_values_ = values;
active_ = false;
return false;
} }
bool set_light_color_values(light::LightColorValues values) {
if (!light_state_has_active_transition_()) {
// Remember the last active light color values. When a transition
// is detected, we'll use these as the starting point. It is not
// possible to use the current values at that point, because the
// transition is already in progress by the time the transition
// is detected.
start_light_values_ = values;
active_ = false;
return false;
}
// When a fresh transition is started, then compute the GPIO outputs
// to use for both the start and end point. This transition handler
// will then transition linearly between these two.
if (is_fresh_transition_()) {
start_->set_light_color_values(start_light_values_);
end_light_values_ = transformer_->get_end_values();
end_->set_light_color_values(end_light_values_);
active_ = true;
}
// When a transition is modified, then use the current GPIO outputs
// as the new starting point.
else if (is_modified_transition_()) {
this->copy_to(start_);
end_light_values_ = transformer_->get_end_values();
end_->set_light_color_values(end_light_values_);
}
light_mode = end_->light_mode;
progress_ = transformer_->get_progress();
// Determine required GPIO outputs for current transition progress.
// In night light mode, do not use actual transitions. Transitioning
// between colors at the very low LED output levels of the night light,
// results in light drops, which are plain ugly to watch.
if (light_mode == "night") {
red = end_->red;
green = end_->green;
blue = end_->blue;
white = end_->white;
}
// In other light modes, apply smooth transitioning.
else {
auto smoothed = light::LightTransitionTransformer::smoothed_progress(progress_);
red = esphome::lerp(smoothed, start_->red, end_->red);
green = esphome::lerp(smoothed, start_->green, end_->green);
blue = esphome::lerp(smoothed, start_->blue, end_->blue);
white = esphome::lerp(smoothed, start_->white, end_->white);
}
return true;
// When a fresh transition is started, then compute the GPIO outputs
// to use for both the start and end point. This transition handler
// will then transition linearly between these two.
if (is_fresh_transition_()) {
start_->set_light_color_values(start_light_values_);
end_light_values_ = transformer_->get_end_values();
end_->set_light_color_values(end_light_values_);
active_ = true;
} }
protected:
bool active_ = false;
float progress_ = 0.0f;
LightStateTransformerInspector *transformer_;
light::LightColorValues start_light_values_;
light::LightColorValues end_light_values_;
GPIOOutputs *start_ = new ColorInstantHandler();
GPIOOutputs *end_ = new ColorInstantHandler();
/**
* Checks if the LightState currently has an active LightTransformer.
*/
bool light_state_has_active_transition_() {
if (!transformer_->is_active())
return false;
if (!transformer_->is_transition())
return false;
return true;
// When a transition is modified, then use the current GPIO outputs
// as the new starting point.
else if (is_modified_transition_()) {
this->copy_to(start_);
end_light_values_ = transformer_->get_end_values();
end_->set_light_color_values(end_light_values_);
} }
/**
* Checks if a fresh transitioning is started.
* A transitioning is fresh when no existing transition is active.
*/
bool is_fresh_transition_() {
return active_ == false;
}
light_mode = end_->light_mode;
progress_ = transformer_->get_progress();
// Determine required GPIO outputs for current transition progress.
/**
* Checks if a new end state is set, while an existing transition
* is active. This might be detected in two ways:
* - the end color has been updated
* - the progress has been reverted
*/
bool is_modified_transition_() {
auto new_end_light_values = transformer_->get_end_values();
auto new_progress = transformer_->get_progress();
return (
new_end_light_values != end_light_values_ ||
new_progress < progress_
);
// In night light mode, do not use actual transitions. Transitioning
// between colors at the very low LED output levels of the night light,
// results in light drops, which are plain ugly to watch.
if (light_mode == "night") {
red = end_->red;
green = end_->green;
blue = end_->blue;
white = end_->white;
} }
// In other light modes, apply smooth transitioning.
else {
auto smoothed = light::LightTransitionTransformer::smoothed_progress(progress_);
red = esphome::lerp(smoothed, start_->red, end_->red);
green = esphome::lerp(smoothed, start_->green, end_->green);
blue = esphome::lerp(smoothed, start_->blue, end_->blue);
white = esphome::lerp(smoothed, start_->white, end_->white);
}
return true;
}
protected:
bool active_ = false;
float progress_ = 0.0f;
LightStateTransformerInspector *transformer_;
light::LightColorValues start_light_values_;
light::LightColorValues end_light_values_;
GPIOOutputs *start_ = new ColorInstantHandler();
GPIOOutputs *end_ = new ColorInstantHandler();
/**
* Checks if the LightState currently has an active LightTransformer.
*/
bool light_state_has_active_transition_() {
if (!transformer_->is_active())
return false;
if (!transformer_->is_transition())
return false;
return true;
}
/**
* Checks if a fresh transitioning is started.
* A transitioning is fresh when no existing transition is active.
*/
bool is_fresh_transition_() { return active_ == false; }
/**
* Checks if a new end state is set, while an existing transition
* is active. This might be detected in two ways:
* - the end color has been updated
* - the progress has been reverted
*/
bool is_modified_transition_() {
auto new_end_light_values = transformer_->get_end_values();
auto new_progress = transformer_->get_progress();
return (new_end_light_values != end_light_values_ || new_progress < progress_);
}
}; };
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 53
- 54
light/color_white_light.h View File

@ -4,7 +4,7 @@
#include <stdexcept> #include <stdexcept>
#include "../common.h" #include "../common.h"
#include "light_modes.h"
#include "light_modes.h"
#include "gpio_outputs.h" #include "gpio_outputs.h"
namespace esphome { namespace esphome {
@ -24,15 +24,16 @@ static const int MIRED_MIN = 153;
static const int MIRED_MAX = 588; static const int MIRED_MAX = 588;
struct RGBWLevelsByTemperature { struct RGBWLevelsByTemperature {
float from_temperature;
float red;
float green;
float blue;
float white;
float from_temperature;
float red;
float green;
float blue;
float white;
}; };
using RGBWLevelsTable = std::array<RGBWLevelsByTemperature, 15>; using RGBWLevelsTable = std::array<RGBWLevelsByTemperature, 15>;
// clang-format off
static const RGBWLevelsTable rgbw_levels_1_ {{ static const RGBWLevelsTable rgbw_levels_1_ {{
{ 501.0f, 0.873f, 0.907f, 1.000f, 0.063f }, { 501.0f, 0.873f, 0.907f, 1.000f, 0.063f },
{ 455.0f, 0.873f, 0.896f, 1.000f, 0.063f }, { 455.0f, 0.873f, 0.896f, 1.000f, 0.063f },
@ -68,62 +69,60 @@ static const RGBWLevelsTable rgbw_levels_100_ {{
{ 154.0f, 1.000f, 0.000f, 0.218f, 0.368f }, { 154.0f, 1.000f, 0.000f, 0.218f, 0.368f },
{ 153.0f, 1.000f, 0.000f, 0.187f, 0.335f } { 153.0f, 1.000f, 0.000f, 0.187f, 0.335f }
}}; }};
// clang-format on
/** /**
* This class can handle the GPIO outputs for the white light mode, * This class can handle the GPIO outputs for the white light mode,
* based on color temperature + brightness. * based on color temperature + brightness.
*/ */
class ColorWhiteLight : public GPIOOutputs { class ColorWhiteLight : public GPIOOutputs {
public:
bool set_light_color_values(light::LightColorValues v) {
light_mode = LIGHT_MODE_WHITE;
public:
bool set_light_color_values(light::LightColorValues v) {
light_mode = LIGHT_MODE_WHITE;
if (v.get_white() == 0.0f) {
return false;
}
auto temperature = clamp_temperature_(v.get_color_temperature());
auto brightness = clamp_brightness_(v.get_brightness());
auto levels_1 = lookup_in_table_(rgbw_levels_1_, temperature);
auto levels_100 = lookup_in_table_(rgbw_levels_100_, temperature);
red = esphome::lerp(brightness, levels_1.red, levels_100.red);
green = esphome::lerp(brightness, levels_1.green, levels_100.green);
blue = esphome::lerp(brightness, levels_1.blue, levels_100.blue);
white = esphome::lerp(brightness, levels_1.white, levels_100.white);
return true;
if (v.get_white() == 0.0f) {
return false;
} }
protected:
float clamp_temperature_(float temperature)
{
if (temperature > MIRED_MAX)
temperature = MIRED_MAX;
else if (temperature < MIRED_MIN)
temperature = MIRED_MIN;
return temperature;
}
float clamp_brightness_(float brightness)
{
if (brightness < 0.01f)
brightness = 0.01f;
else if (brightness > 1.00f)
brightness = 1.00f;
return brightness;
}
RGBWLevelsByTemperature lookup_in_table_(RGBWLevelsTable table, float temperature)
{
for (RGBWLevelsByTemperature& item : table)
if (temperature >= item.from_temperature)
return item;
throw std::invalid_argument("received too low temperature");
}
auto temperature = clamp_temperature_(v.get_color_temperature());
auto brightness = clamp_brightness_(v.get_brightness());
auto levels_1 = lookup_in_table_(rgbw_levels_1_, temperature);
auto levels_100 = lookup_in_table_(rgbw_levels_100_, temperature);
red = esphome::lerp(brightness, levels_1.red, levels_100.red);
green = esphome::lerp(brightness, levels_1.green, levels_100.green);
blue = esphome::lerp(brightness, levels_1.blue, levels_100.blue);
white = esphome::lerp(brightness, levels_1.white, levels_100.white);
return true;
}
protected:
float clamp_temperature_(float temperature) {
if (temperature > MIRED_MAX)
temperature = MIRED_MAX;
else if (temperature < MIRED_MIN)
temperature = MIRED_MIN;
return temperature;
}
float clamp_brightness_(float brightness) {
if (brightness < 0.01f)
brightness = 0.01f;
else if (brightness > 1.00f)
brightness = 1.00f;
return brightness;
}
RGBWLevelsByTemperature lookup_in_table_(RGBWLevelsTable table, float temperature) {
for (RGBWLevelsByTemperature& item : table)
if (temperature >= item.from_temperature)
return item;
throw std::invalid_argument("received too low temperature");
}
}; };
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 30
- 33
light/gpio_outputs.h View File

@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "light_modes.h" #include "light_modes.h"
@ -12,39 +12,36 @@ namespace bslamp2 {
* the requested color on the physical device. * the requested color on the physical device.
*/ */
class GPIOOutputs { class GPIOOutputs {
public:
float red = 0.0f;
float green = 0.0f;
float blue = 0.0f;
float white = 0.0f;
std::string light_mode = LIGHT_MODE_OFF;
public:
float red = 0.0f;
float green = 0.0f;
float blue = 0.0f;
float white = 0.0f;
std::string light_mode = LIGHT_MODE_OFF;
/**
* Sets the red, green, blue, white fields to the PWM duty cycles
* that are required to represent the requested light color for
* the provided LightColorValues input.
*
* Returns true when the input can be handled, false otherwise.
*/
virtual bool set_light_color_values(light::LightColorValues v) = 0;
/**
* Sets the red, green, blue, white fields to the PWM duty cycles
* that are required to represent the requested light color for
* the provided LightColorValues input.
*
* Returns true when the input can be handled, false otherwise.
*/
virtual bool set_light_color_values(light::LightColorValues v) = 0;
/**
* Copies the current output values to another GPIOOutputs object.
*/
void copy_to(GPIOOutputs *other) {
other->red = red;
other->green = green;
other->blue = blue;
other->white = white;
other->light_mode = light_mode;
}
/**
* Copies the current output values to another GPIOOutputs object.
*/
void copy_to(GPIOOutputs *other) {
other->red = red;
other->green = green;
other->blue = blue;
other->white = white;
other->light_mode = light_mode;
}
void log(const char *prefix) {
ESP_LOGD(TAG, "%s: RGB=[%f,%f,%f], white=%f",
prefix, red, green, blue, white);
}
void log(const char *prefix) { ESP_LOGD(TAG, "%s: RGB=[%f,%f,%f], white=%f", prefix, red, green, blue, white); }
}; };
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 9
- 9
light/light_modes.h View File

@ -1,15 +1,15 @@
#pragma once
#pragma once
namespace esphome { namespace esphome {
namespace xiaomi { namespace xiaomi {
namespace bslamp2 { namespace bslamp2 {
static const std::string LIGHT_MODE_UNKNOWN { "unknown" };
static const std::string LIGHT_MODE_OFF { "off" };
static const std::string LIGHT_MODE_RGB { "rgb" };
static const std::string LIGHT_MODE_WHITE { "white" };
static const std::string LIGHT_MODE_NIGHT { "night" };
static const std::string LIGHT_MODE_UNKNOWN{"unknown"};
static const std::string LIGHT_MODE_OFF{"off"};
static const std::string LIGHT_MODE_RGB{"rgb"};
static const std::string LIGHT_MODE_WHITE{"white"};
static const std::string LIGHT_MODE_NIGHT{"night"};
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 75
- 82
light/light_output.h View File

@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "../common.h" #include "../common.h"
#include "../light_hal.h" #include "../light_hal.h"
@ -19,96 +19,89 @@ namespace bslamp2 {
* logical light color input. * logical light color input.
*/ */
class XiaomiBslamp2LightOutput : public Component, public light::LightOutput { class XiaomiBslamp2LightOutput : public Component, public light::LightOutput {
public:
void set_parent(LightHAL *light) { light_ = light; }
public:
void set_parent(LightHAL *light) { light_ = light; }
/**
* Returns a LightTraits object, which is used to explain to the outside
* world (e.g. Home Assistant) what features are supported by this device.
*/
light::LightTraits get_traits() override
{
auto traits = light::LightTraits();
traits.set_supports_rgb(true);
traits.set_supports_color_temperature(true);
traits.set_supports_brightness(true);
traits.set_supports_rgb_white_value(false);
traits.set_supports_color_interlock(true);
traits.set_min_mireds(MIRED_MIN);
traits.set_max_mireds(MIRED_MAX);
return traits;
}
/**
* Returns a LightTraits object, which is used to explain to the outside
* world (e.g. Home Assistant) what features are supported by this device.
*/
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
traits.set_supports_rgb(true);
traits.set_supports_color_temperature(true);
traits.set_supports_brightness(true);
traits.set_supports_rgb_white_value(false);
traits.set_supports_color_interlock(true);
traits.set_min_mireds(MIRED_MIN);
traits.set_max_mireds(MIRED_MAX);
return traits;
}
void add_on_light_mode_callback(std::function<void(std::string)> &&callback) {
light_mode_callback_.add(std::move(callback));
}
void add_on_light_mode_callback(std::function<void(std::string)> &&callback) {
light_mode_callback_.add(std::move(callback));
}
void add_on_state_callback(std::function<void(light::LightColorValues)> &&callback) {
state_callback_.add(std::move(callback));
}
void add_on_state_callback(std::function<void(light::LightColorValues)> &&callback) {
state_callback_.add(std::move(callback));
}
/**
* Applies a requested light state to the physicial GPIO outputs.
*/
void write_state(light::LightState *state)
{
auto values = state->current_values;
/**
* Applies a requested light state to the physicial GPIO outputs.
*/
void write_state(light::LightState *state) {
auto values = state->current_values;
// The color must either be set instantly, or the color is
// transitioning to an end color. The transition handler will do its
// own inspection to see if a transition is currently active or not.
// Based on the outcome, use either the instant or transition handler.
GPIOOutputs *delegate;
if (transition_handler_->set_light_color_values(values)) {
delegate = transition_handler_;
light_mode_callback_.call(delegate->light_mode);
state_callback_.call(transition_handler_->get_end_values());
} else {
instant_handler_->set_light_color_values(values);
delegate = instant_handler_;
light_mode_callback_.call(delegate->light_mode);
state_callback_.call(values);
}
// The color must either be set instantly, or the color is
// transitioning to an end color. The transition handler will do its
// own inspection to see if a transition is currently active or not.
// Based on the outcome, use either the instant or transition handler.
GPIOOutputs *delegate;
if (transition_handler_->set_light_color_values(values)) {
delegate = transition_handler_;
light_mode_callback_.call(delegate->light_mode);
state_callback_.call(transition_handler_->get_end_values());
} else {
instant_handler_->set_light_color_values(values);
delegate = instant_handler_;
light_mode_callback_.call(delegate->light_mode);
state_callback_.call(values);
}
// Note: one might think that it is more logical to turn on the LED
// circuitry master switch after setting the individual channels,
// but this is the order that was used by the original firmware. I
// tried to stay as close as possible to the original behavior, so
// that's why these GPIOs are turned on at this point.
if (values.get_state() != 0)
light_->turn_on();
// Note: one might think that it is more logical to turn on the LED
// circuitry master switch after setting the individual channels,
// but this is the order that was used by the original firmware. I
// tried to stay as close as possible to the original behavior, so
// that's why these GPIOs are turned on at this point.
if (values.get_state() != 0)
light_->turn_on();
// Apply the current GPIO output levels from the selected handler.
light_->set_rgbw(
delegate->red,
delegate->green,
delegate->blue,
delegate->white
);
// Apply the current GPIO output levels from the selected handler.
light_->set_rgbw(delegate->red, delegate->green, delegate->blue, delegate->white);
if (values.get_state() == 0)
light_->turn_off();
}
if (values.get_state() == 0)
light_->turn_off();
}
protected:
LightHAL *light_;
ColorTransitionHandler *transition_handler_;
ColorInstantHandler *instant_handler_ = new ColorInstantHandler();
CallbackManager<void(std::string)> light_mode_callback_{};
CallbackManager<void(light::LightColorValues)> state_callback_{};
protected:
LightHAL *light_;
ColorTransitionHandler *transition_handler_;
ColorInstantHandler *instant_handler_ = new ColorInstantHandler();
CallbackManager<void(std::string)> light_mode_callback_{};
CallbackManager<void(light::LightColorValues)> state_callback_{};
friend class XiaomiBslamp2LightState;
friend class XiaomiBslamp2LightState;
/**
* Called by the XiaomiBslamp2LightState class, to set the object that can be
* used to access the protected LightTransformer data from the LightState
* object.
*/
void set_transformer_inspector(LightStateTransformerInspector *exposer) {
transition_handler_ = new ColorTransitionHandler(exposer);
}
/**
* Called by the XiaomiBslamp2LightState class, to set the object that can be
* used to access the protected LightTransformer data from the LightState
* object.
*/
void set_transformer_inspector(LightStateTransformerInspector *exposer) {
transition_handler_ = new ColorTransitionHandler(exposer);
}
}; };
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 14
- 15
light/light_state.h View File

@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "../common.h" #include "../common.h"
@ -13,19 +13,18 @@ namespace bslamp2 {
* This class is used by the ColorTransitionHandler class to inspect if * This class is used by the ColorTransitionHandler class to inspect if
* an ongoing light color transition is active in the LightState object. * an ongoing light color transition is active in the LightState object.
*/ */
class XiaomiBslamp2LightState : public light::LightState, public LightStateTransformerInspector
{
public:
XiaomiBslamp2LightState(const std::string &name, XiaomiBslamp2LightOutput *output) : light::LightState(name, output) {
output->set_transformer_inspector(this);
}
class XiaomiBslamp2LightState : public light::LightState, public LightStateTransformerInspector {
public:
XiaomiBslamp2LightState(const std::string &name, XiaomiBslamp2LightOutput *output) : light::LightState(name, output) {
output->set_transformer_inspector(this);
}
bool is_active() { return transformer_ != nullptr; }
bool is_transition() { return transformer_->is_transition(); }
light::LightColorValues get_end_values() { return transformer_->get_end_values(); }
float get_progress() { return transformer_->get_progress(); }
bool is_active() { return transformer_ != nullptr; }
bool is_transition() { return transformer_->is_transition(); }
light::LightColorValues get_end_values() { return transformer_->get_end_values(); }
float get_progress() { return transformer_->get_progress(); }
}; };
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 161
- 181
light/presets.h View File

@ -8,203 +8,183 @@ namespace xiaomi {
namespace bslamp2 { namespace bslamp2 {
class Preset : public Component { class Preset : public Component {
public:
std::string group_name;
std::string name;
Preset *next_preset = nullptr;
explicit Preset(light::LightState *light, std::string group_name, std::string name):
group_name(group_name), name(name), light_state_(light) {}
void set_transition_length(uint32_t t) { transition_length_ = t; }
void set_brightness(float t) { brightness_ = t; }
void set_red(float t) { red_ = t; }
void set_green(float t) { green_ = t; }
void set_blue(float t) { blue_ = t; }
void set_color_temperature(float t) { color_temperature_ = t; }
void set_effect(const std::string &effect) { effect_ = effect; }
void apply() {
ESP_LOGI(TAG, "Activating light preset: %s/%s",
group_name.c_str(), name.c_str());
auto call = light_state_->make_call();
call.set_state(true);
if (transition_length_.has_value()) {
call.set_transition_length(*transition_length_);
}
if (brightness_.has_value()) {
call.set_brightness(*brightness_);
}
if (red_.has_value()) {
call.set_red(*red_);
}
if (green_.has_value()) {
call.set_green(*green_);
}
if (blue_.has_value()) {
call.set_blue(*blue_);
}
if (color_temperature_.has_value()) {
call.set_color_temperature(*color_temperature_);
}
if (effect_.has_value()) {
call.set_effect(*effect_);
}
call.perform();
}
protected:
light::LightState *light_state_;
optional<uint32_t> transition_length_;
optional<float> brightness_;
optional<float> red_;
optional<float> green_;
optional<float> blue_;
optional<float> color_temperature_;
optional<std::string> effect_;
public:
std::string group_name;
std::string name;
Preset *next_preset = nullptr;
explicit Preset(light::LightState *light, std::string group_name, std::string name)
: group_name(group_name), name(name), light_state_(light) {}
void set_transition_length(uint32_t t) { transition_length_ = t; }
void set_brightness(float t) { brightness_ = t; }
void set_red(float t) { red_ = t; }
void set_green(float t) { green_ = t; }
void set_blue(float t) { blue_ = t; }
void set_color_temperature(float t) { color_temperature_ = t; }
void set_effect(const std::string &effect) { effect_ = effect; }
void apply() {
ESP_LOGI(TAG, "Activating light preset: %s/%s", group_name.c_str(), name.c_str());
auto call = light_state_->make_call();
call.set_state(true);
if (transition_length_.has_value())
call.set_transition_length(*transition_length_);
if (brightness_.has_value())
call.set_brightness(*brightness_);
if (red_.has_value())
call.set_red(*red_);
if (green_.has_value())
call.set_green(*green_);
if (blue_.has_value())
call.set_blue(*blue_);
if (color_temperature_.has_value())
call.set_color_temperature(*color_temperature_);
if (effect_.has_value())
call.set_effect(*effect_);
call.perform();
}
protected:
light::LightState *light_state_;
optional<uint32_t> transition_length_;
optional<float> brightness_;
optional<float> red_;
optional<float> green_;
optional<float> blue_;
optional<float> color_temperature_;
optional<std::string> effect_;
}; };
class PresetGroup { class PresetGroup {
public:
std::string name;
PresetGroup *next_group = nullptr;
Preset *first_preset = nullptr;
Preset *last_preset = nullptr;
Preset *active_preset = nullptr;
explicit PresetGroup(std::string g_name) : name(g_name) {}
void add_preset(Preset *p) {
if (first_preset == nullptr) {
first_preset = last_preset = active_preset = p;
} else {
last_preset->next_preset = p;
last_preset = p;
}
}
Preset *get_preset(std::string p_name) {
for (auto p = first_preset; p != nullptr; p = p->next_preset) {
if (p->name == p_name) {
return p;
}
}
return nullptr;
public:
std::string name;
PresetGroup *next_group = nullptr;
Preset *first_preset = nullptr;
Preset *last_preset = nullptr;
Preset *active_preset = nullptr;
explicit PresetGroup(std::string g_name) : name(g_name) {}
void add_preset(Preset *p) {
if (first_preset == nullptr) {
first_preset = last_preset = active_preset = p;
} else {
last_preset->next_preset = p;
last_preset = p;
} }
}
Preset *get_preset(std::string p_name) {
for (auto p = first_preset; p != nullptr; p = p->next_preset)
if (p->name == p_name)
return p;
return nullptr;
}
}; };
class PresetsContainer : public Component { class PresetsContainer : public Component {
public:
PresetGroup *first_group = nullptr;
PresetGroup *last_group = nullptr;
PresetGroup *active_group = nullptr;
void dump_config() {
if (first_group == nullptr) {
return;
}
ESP_LOGCONFIG(TAG, "Light Presets:");
for (auto g = first_group; g != nullptr; g = g->next_group) {
ESP_LOGCONFIG(TAG, " Preset group: %s", g->name.c_str());
for (auto p = g->first_preset; p != nullptr; p = p->next_preset) {
ESP_LOGCONFIG(TAG, " Preset: %s", p->name.c_str());
}
}
public:
PresetGroup *first_group = nullptr;
PresetGroup *last_group = nullptr;
PresetGroup *active_group = nullptr;
void dump_config() {
if (first_group != nullptr) {
ESP_LOGCONFIG(TAG, "Light Presets:");
for (auto g = first_group; g != nullptr; g = g->next_group) {
ESP_LOGCONFIG(TAG, " Preset group: %s", g->name.c_str());
for (auto p = g->first_preset; p != nullptr; p = p->next_preset)
ESP_LOGCONFIG(TAG, " Preset: %s", p->name.c_str());
}
} }
}
void add_preset(Preset *preset) {
auto g = make_preset_group_(preset->group_name);
g->add_preset(preset);
}
void add_preset(Preset *preset) {
auto g = make_preset_group_(preset->group_name);
g->add_preset(preset);
}
PresetGroup *get_group(std::string g_name) {
for (auto g = first_group; g != nullptr; g = g->next_group) {
if (g->name == g_name) {
return g;
}
}
return nullptr;
}
PresetGroup *get_group(std::string g_name) {
for (auto g = first_group; g != nullptr; g = g->next_group)
if (g->name == g_name)
return g;
return nullptr;
}
void activate_next_group() {
if (active_group == nullptr) {
ESP_LOGW(TAG, "activate_next_group(): no preset groups defined");
return;
}
active_group = active_group->next_group == nullptr
? first_group : active_group->next_group;
if (active_group->active_preset == nullptr) {
ESP_LOGW(TAG, "activate_next_group(): no presets defined for group %s",
active_group->name.c_str());
return;
}
active_group->active_preset->apply();
void activate_next_group() {
if (active_group == nullptr) {
ESP_LOGW(TAG, "activate_next_group(): no preset groups defined");
return;
} }
void activate_next_preset() {
if (active_group == nullptr) {
ESP_LOGW(TAG, "activate_next_preset(): no preset groups defined");
return;
}
auto p = active_group->active_preset;
if (p == nullptr) {
ESP_LOGW(TAG, "activate_next_preset(): no presets defined for group %s",
active_group->name.c_str());
return;
}
active_group->active_preset = p->next_preset == nullptr
? active_group->first_preset : p->next_preset;
active_group->active_preset->apply();
active_group = active_group->next_group == nullptr ? first_group : active_group->next_group;
if (active_group->active_preset == nullptr) {
ESP_LOGW(TAG, "activate_next_group(): no presets defined for group %s", active_group->name.c_str());
return;
} }
active_group->active_preset->apply();
}
void activate_group(std::string g_name) {
auto g = get_group(g_name);
if (g == nullptr) {
ESP_LOGE(TAG, "activate_group(%s): preset group does not exist",
g_name.c_str());
return;
}
auto p = g->active_preset;
if (p == nullptr) {
ESP_LOGW(TAG, "activate_group(%s): no presets defined for group",
g_name.c_str());
return;
}
p->apply();
void activate_next_preset() {
if (active_group == nullptr) {
ESP_LOGW(TAG, "activate_next_preset(): no preset groups defined");
return;
} }
void activate_preset(std::string g_name, std::string p_name) {
auto g = get_group(g_name);
if (g == nullptr) {
ESP_LOGE(TAG, "activate_preset(%s, %s): preset group '%s' does not exist",
g_name.c_str(), p_name.c_str(), g_name.c_str());
return;
}
auto p = g->get_preset(p_name);
if (p == nullptr) {
ESP_LOGE(TAG, "activate_preset(%s, %s): preset '%s' does not exist in group '%s'",
g_name.c_str(), p_name.c_str(), p_name.c_str(), g->name.c_str());
return;
}
p->apply();
auto p = active_group->active_preset;
if (p == nullptr) {
ESP_LOGW(TAG, "activate_next_preset(): no presets defined for group %s", active_group->name.c_str());
return;
} }
protected:
PresetGroup *make_preset_group_(std::string g_name) {
auto g = get_group(g_name);
if (g == nullptr) {
g = new PresetGroup(g_name);
if (first_group == nullptr) {
first_group = last_group = active_group = g;
} else {
last_group->next_group = g;
last_group = g;
}
}
return g;
active_group->active_preset = p->next_preset == nullptr ? active_group->first_preset : p->next_preset;
active_group->active_preset->apply();
}
void activate_group(std::string g_name) {
auto g = get_group(g_name);
if (g == nullptr) {
ESP_LOGE(TAG, "activate_group(%s): preset group does not exist", g_name.c_str());
return;
}
auto p = g->active_preset;
if (p == nullptr) {
ESP_LOGW(TAG, "activate_group(%s): no presets defined for group", g_name.c_str());
return;
}
p->apply();
}
void activate_preset(std::string g_name, std::string p_name) {
auto g = get_group(g_name);
if (g == nullptr) {
ESP_LOGE(TAG, "activate_preset(%s, %s): preset group '%s' does not exist", g_name.c_str(), p_name.c_str(),
g_name.c_str());
return;
}
auto p = g->get_preset(p_name);
if (p == nullptr) {
ESP_LOGE(TAG, "activate_preset(%s, %s): preset '%s' does not exist in group '%s'", g_name.c_str(), p_name.c_str(),
p_name.c_str(), g->name.c_str());
return;
}
p->apply();
}
protected:
PresetGroup *make_preset_group_(std::string g_name) {
auto g = get_group(g_name);
if (g == nullptr) {
g = new PresetGroup(g_name);
if (first_group == nullptr) {
first_group = last_group = active_group = g;
} else {
last_group->next_group = g;
last_group = g;
}
} }
return g;
}
}; };
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 38
- 38
light_hal.h View File

@ -1,48 +1,48 @@
#pragma once
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/ledc/ledc_output.h"
#include "esphome/components/gpio/output/gpio_binary_output.h" #include "esphome/components/gpio/output/gpio_binary_output.h"
#include "esphome/components/ledc/ledc_output.h"
#include "esphome/core/component.h"
namespace esphome { namespace esphome {
namespace xiaomi { namespace xiaomi {
namespace bslamp2 { namespace bslamp2 {
class LightHAL : Component { class LightHAL : Component {
public:
void set_red_pin(ledc::LEDCOutput *pin) { red_ = pin; }
void set_green_pin(ledc::LEDCOutput *pin) { green_ = pin; }
void set_blue_pin(ledc::LEDCOutput *pin) { blue_ = pin; }
void set_white_pin(ledc::LEDCOutput *pin) { white_ = pin; }
void set_master1_pin(gpio::GPIOBinaryOutput *pin) { master1_ = pin; }
void set_master2_pin(gpio::GPIOBinaryOutput *pin) { master2_ = pin; }
void turn_on() {
master1_->turn_on();
master2_->turn_on();
}
void turn_off() {
master1_->turn_off();
master2_->turn_off();
}
void set_rgbw(float r, float g, float b, float w) {
red_->set_level(r);
green_->set_level(g);
blue_->set_level(b);
white_->set_level(w);
}
protected:
ledc::LEDCOutput *red_;
ledc::LEDCOutput *green_;
ledc::LEDCOutput *blue_;
ledc::LEDCOutput *white_;
gpio::GPIOBinaryOutput *master1_;
gpio::GPIOBinaryOutput *master2_;
public:
void set_red_pin(ledc::LEDCOutput *pin) { red_ = pin; }
void set_green_pin(ledc::LEDCOutput *pin) { green_ = pin; }
void set_blue_pin(ledc::LEDCOutput *pin) { blue_ = pin; }
void set_white_pin(ledc::LEDCOutput *pin) { white_ = pin; }
void set_master1_pin(gpio::GPIOBinaryOutput *pin) { master1_ = pin; }
void set_master2_pin(gpio::GPIOBinaryOutput *pin) { master2_ = pin; }
void turn_on() {
master1_->turn_on();
master2_->turn_on();
}
void turn_off() {
master1_->turn_off();
master2_->turn_off();
}
void set_rgbw(float r, float g, float b, float w) {
red_->set_level(r);
green_->set_level(g);
blue_->set_level(b);
white_->set_level(w);
}
protected:
ledc::LEDCOutput *red_;
ledc::LEDCOutput *green_;
ledc::LEDCOutput *blue_;
ledc::LEDCOutput *white_;
gpio::GPIOBinaryOutput *master1_;
gpio::GPIOBinaryOutput *master2_;
}; };
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 11
- 13
output/output.h View File

@ -1,9 +1,9 @@
#pragma once
#pragma once
#include <cmath>
#include "../common.h" #include "../common.h"
#include "../front_panel_hal.h" #include "../front_panel_hal.h"
#include "esphome/components/output/float_output.h" #include "esphome/components/output/float_output.h"
#include <cmath>
namespace esphome { namespace esphome {
namespace xiaomi { namespace xiaomi {
@ -14,17 +14,15 @@ namespace bslamp2 {
* level indicator on the Xiaomi Mijia Bedside Lamp 2 front panel. * level indicator on the Xiaomi Mijia Bedside Lamp 2 front panel.
*/ */
class XiaomiBslamp2FrontPanelLight : public output::FloatOutput, public Component { class XiaomiBslamp2FrontPanelLight : public output::FloatOutput, public Component {
public:
void set_parent(FrontPanelHAL *front_panel) { front_panel_ = front_panel; }
public:
void set_parent(FrontPanelHAL *front_panel) { front_panel_ = front_panel; }
void write_state(float level) {
front_panel_->set_light_level(level);
}
void write_state(float level) { front_panel_->set_light_level(level); }
protected:
FrontPanelHAL *front_panel_;
protected:
FrontPanelHAL *front_panel_;
}; };
} // namespace bslamp2
} // namespace yxiaomi
} // namespace esphome
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 43
- 45
sensor/slider_sensor.h View File

@ -1,16 +1,16 @@
#pragma once
#pragma once
#include <cmath>
#include "../common.h" #include "../common.h"
#include "../front_panel_hal.h" #include "../front_panel_hal.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include <cmath>
namespace esphome { namespace esphome {
namespace xiaomi { namespace xiaomi {
namespace bslamp2 { namespace bslamp2 {
/** /**
* A sensor for the touch slider on the front panel of the
* A sensor for the touch slider on the front panel of the
* Xiaomi Mijia Bedside Lamp 2. * Xiaomi Mijia Bedside Lamp 2.
* *
* This sensor publishes the level at which the slider was touched, so it * This sensor publishes the level at which the slider was touched, so it
@ -20,46 +20,44 @@ namespace bslamp2 {
* panel illumination (this is implemented by the slider output component). * panel illumination (this is implemented by the slider output component).
*/ */
class XiaomiBslamp2SliderSensor : public sensor::Sensor, public Component { class XiaomiBslamp2SliderSensor : public sensor::Sensor, public Component {
public:
void set_parent(FrontPanelHAL *front_panel) { front_panel_ = front_panel; }
void set_range_from(float from) { range_from_ = from; }
void set_range_to(float to) { range_to_ = to; }
void setup() {
slope_ = (range_to_ - range_from_) / 19.0f;
front_panel_->add_on_event_callback(
[this](EVENT ev) {
if ((ev & FLAG_PART_MASK) == FLAG_PART_SLIDER) {
float level = (ev & FLAG_LEVEL_MASK) >> FLAG_LEVEL_SHIFT;
// Slider level 1 is really hard to touch. It is between
// the power button and the slider space, so it doesn't
// look like this one was ever meant to be used, or that
// the design was faulty on this. Therefore, level 1 is
// ignored. The resulting range of levels is 0-19.
float corrected_level = max(0.0f, level - 2.0f);
float final_level = range_from_ + (slope_ * corrected_level);
this->publish_state(final_level);
}
}
);
}
void dump_config() {
ESP_LOGCONFIG(TAG, "Front panel slider sensor:");
ESP_LOGCONFIG(TAG, " Range from: %f", range_from_);
ESP_LOGCONFIG(TAG, " Range to: %f", range_to_);
}
protected:
FrontPanelHAL *front_panel_;
float range_from_;
float range_to_;
float slope_;
public:
void set_parent(FrontPanelHAL *front_panel) { front_panel_ = front_panel; }
void set_range_from(float from) { range_from_ = from; }
void set_range_to(float to) { range_to_ = to; }
void setup() {
slope_ = (range_to_ - range_from_) / 19.0f;
front_panel_->add_on_event_callback([this](EVENT ev) {
if ((ev & FLAG_PART_MASK) == FLAG_PART_SLIDER) {
float level = (ev & FLAG_LEVEL_MASK) >> FLAG_LEVEL_SHIFT;
// Slider level 1 is really hard to touch. It is between
// the power button and the slider space, so it doesn't
// look like this one was ever meant to be used, or that
// the design was faulty on this. Therefore, level 1 is
// ignored. The resulting range of levels is 0-19.
float corrected_level = max(0.0f, level - 2.0f);
float final_level = range_from_ + (slope_ * corrected_level);
this->publish_state(final_level);
}
});
}
void dump_config() {
ESP_LOGCONFIG(TAG, "Front panel slider sensor:");
ESP_LOGCONFIG(TAG, " Range from: %f", range_from_);
ESP_LOGCONFIG(TAG, " Range to: %f", range_to_);
}
protected:
FrontPanelHAL *front_panel_;
float range_from_;
float range_to_;
float slope_;
}; };
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

+ 18
- 18
text_sensor/light_mode_text_sensor.h View File

@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/text_sensor/text_sensor.h"
@ -13,23 +13,23 @@ namespace bslamp2 {
* The possible light modes are "off", "rgb", "white" and "night". * The possible light modes are "off", "rgb", "white" and "night".
*/ */
class XiaomiBslamp2LightModeTextSensor : public text_sensor::TextSensor, public Component { class XiaomiBslamp2LightModeTextSensor : public text_sensor::TextSensor, public Component {
public:
void set_parent(XiaomiBslamp2LightOutput *light) { light_ = light; }
public:
void set_parent(XiaomiBslamp2LightOutput *light) { light_ = light; }
void setup() {
light_->add_on_light_mode_callback([this](std::string light_mode) {
if (last_light_mode_ != light_mode) {
publish_state(light_mode);
last_light_mode_ = light_mode;
}
});
}
void setup() {
light_->add_on_light_mode_callback([this](std::string light_mode) {
if (last_light_mode_ != light_mode) {
publish_state(light_mode);
last_light_mode_ = light_mode;
}
});
}
protected:
XiaomiBslamp2LightOutput *light_;
std::string last_light_mode_ = LIGHT_MODE_UNKNOWN;
protected:
XiaomiBslamp2LightOutput *light_;
std::string last_light_mode_ = LIGHT_MODE_UNKNOWN;
}; };
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome
} // namespace bslamp2
} // namespace xiaomi
} // namespace esphome

Loading…
Cancel
Save