Added lamp
This commit is contained in:
+116
@@ -0,0 +1,116 @@
|
|||||||
|
#https://github.com/mmakaay/esphome-xiaomi_bslamp2/blob/dev/packages/core-dev.yaml
|
||||||
|
esp32:
|
||||||
|
board: esp32doit-devkit-v1
|
||||||
|
framework:
|
||||||
|
type: esp-idf
|
||||||
|
sdkconfig_options:
|
||||||
|
CONFIG_FREERTOS_UNICORE: y
|
||||||
|
advanced:
|
||||||
|
ignore_efuse_custom_mac: true
|
||||||
|
ignore_efuse_mac_crc: true
|
||||||
|
|
||||||
|
external_components:
|
||||||
|
- source:
|
||||||
|
type: local
|
||||||
|
path: ../components
|
||||||
|
|
||||||
|
ota:
|
||||||
|
on_begin:
|
||||||
|
then:
|
||||||
|
- light.disco_on:
|
||||||
|
id: my_light
|
||||||
|
red: 0%
|
||||||
|
green: 0%
|
||||||
|
blue: 100%
|
||||||
|
brightness: 2%
|
||||||
|
transition_length: 0s
|
||||||
|
|
||||||
|
on_progress:
|
||||||
|
then:
|
||||||
|
- front_panel.set_level: !lambda return (x / 100.0f);
|
||||||
|
- front_panel.update_leds:
|
||||||
|
|
||||||
|
on_end:
|
||||||
|
then:
|
||||||
|
- light.disco_on:
|
||||||
|
id: my_light
|
||||||
|
red: 0%
|
||||||
|
green: 100%
|
||||||
|
blue: 0%
|
||||||
|
brightness: 2%
|
||||||
|
transition_length: 0s
|
||||||
|
|
||||||
|
on_error:
|
||||||
|
then:
|
||||||
|
- light.disco_on:
|
||||||
|
id: my_light
|
||||||
|
red: 100%
|
||||||
|
green: 0%
|
||||||
|
blue: 0%
|
||||||
|
brightness: 2%
|
||||||
|
- delay: 1s
|
||||||
|
- light.disco_off:
|
||||||
|
id: my_light
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Hardware setup
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
i2c:
|
||||||
|
id: front_panel_i2c
|
||||||
|
sda: GPIO21
|
||||||
|
scl: GPIO19
|
||||||
|
scan: true
|
||||||
|
|
||||||
|
output:
|
||||||
|
- platform: ledc
|
||||||
|
id: output_red
|
||||||
|
pin: GPIO13
|
||||||
|
frequency: 3000
|
||||||
|
channel: 0
|
||||||
|
- platform: ledc
|
||||||
|
id: output_green
|
||||||
|
pin: GPIO14
|
||||||
|
frequency: 3000
|
||||||
|
channel: 1
|
||||||
|
- platform: ledc
|
||||||
|
id: output_blue
|
||||||
|
pin:
|
||||||
|
number: GPIO5
|
||||||
|
ignore_strapping_warning: true
|
||||||
|
frequency: 3000
|
||||||
|
channel: 2
|
||||||
|
- platform: ledc
|
||||||
|
id: output_white
|
||||||
|
pin:
|
||||||
|
number: GPIO12
|
||||||
|
ignore_strapping_warning: true
|
||||||
|
frequency: 10000
|
||||||
|
channel: 4
|
||||||
|
- platform: gpio
|
||||||
|
id: output_master1
|
||||||
|
pin: GPIO33
|
||||||
|
- platform: gpio
|
||||||
|
id: output_master2
|
||||||
|
pin:
|
||||||
|
number: GPIO4
|
||||||
|
mode: OUTPUT
|
||||||
|
# This float output can be used to control the front panel illumination +
|
||||||
|
# level indicator LEDs. Value 0.0 turns off the illumination. Other values
|
||||||
|
# (up to 1.0) turn on the illumination and set the level indicator to the
|
||||||
|
# requested level.
|
||||||
|
- platform: xiaomi_bslamp2
|
||||||
|
id: my_front_panel_illumination
|
||||||
|
|
||||||
|
xiaomi_bslamp2:
|
||||||
|
light:
|
||||||
|
red: output_red
|
||||||
|
green: output_green
|
||||||
|
blue: output_blue
|
||||||
|
white: output_white
|
||||||
|
master1: output_master1
|
||||||
|
master2: output_master2
|
||||||
|
front_panel:
|
||||||
|
i2c: front_panel_i2c
|
||||||
|
address: 0x2C
|
||||||
|
trigger_pin: GPIO16
|
||||||
@@ -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
|
||||||
@@ -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: ''
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome import pins
|
||||||
|
from esphome.components.ledc.output import LEDCOutput
|
||||||
|
from esphome.components.gpio.output import GPIOBinaryOutput
|
||||||
|
from esphome.components.i2c import I2CBus, I2CDevice
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_LIGHT,
|
||||||
|
CONF_RED,
|
||||||
|
CONF_GREEN,
|
||||||
|
CONF_BLUE,
|
||||||
|
CONF_WHITE,
|
||||||
|
CONF_I2C,
|
||||||
|
CONF_ADDRESS,
|
||||||
|
CONF_TRIGGER_PIN,
|
||||||
|
)
|
||||||
|
from esphome.util import parse_esphome_version
|
||||||
|
from voluptuous import Invalid
|
||||||
|
|
||||||
|
CODEOWNERS = ["@mmakaay"]
|
||||||
|
|
||||||
|
CONF_MASTER1 = "master1"
|
||||||
|
CONF_MASTER2 = "master2"
|
||||||
|
CONF_FRONT_PANEL = "front_panel"
|
||||||
|
CONF_LIGHT_HAL_ID = "light_hal_id"
|
||||||
|
CONF_FRONT_PANEL_HAL_ID = "front_panel_hal_id"
|
||||||
|
|
||||||
|
xiaomi_ns = cg.esphome_ns.namespace("xiaomi")
|
||||||
|
bslamp2_ns = xiaomi_ns.namespace("bslamp2")
|
||||||
|
LightHAL = bslamp2_ns.class_("LightHAL", cg.Component)
|
||||||
|
FrontPanelHAL = bslamp2_ns.class_("FrontPanelHAL", cg.Component, I2CDevice)
|
||||||
|
|
||||||
|
FrontPanelLEDs = bslamp2_ns.enum("FrontPanelLEDs")
|
||||||
|
FRONT_PANEL_LED_OPTIONS = {
|
||||||
|
"NONE": FrontPanelLEDs.LED_NONE,
|
||||||
|
"ALL": FrontPanelLEDs.LED_ALL,
|
||||||
|
"POWER": FrontPanelLEDs.LED_POWER,
|
||||||
|
"COLOR": FrontPanelLEDs.LED_COLOR,
|
||||||
|
"1": FrontPanelLEDs.LED_1,
|
||||||
|
"2": FrontPanelLEDs.LED_2,
|
||||||
|
"3": FrontPanelLEDs.LED_3,
|
||||||
|
"4": FrontPanelLEDs.LED_4,
|
||||||
|
"5": FrontPanelLEDs.LED_5,
|
||||||
|
"6": FrontPanelLEDs.LED_6,
|
||||||
|
"7": FrontPanelLEDs.LED_7,
|
||||||
|
"8": FrontPanelLEDs.LED_8,
|
||||||
|
"9": FrontPanelLEDs.LED_9,
|
||||||
|
"10": FrontPanelLEDs.LED_10,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def check_version_compatibility(config):
|
||||||
|
esphome_version = parse_esphome_version()
|
||||||
|
if esphome_version < (2026, 4, 0):
|
||||||
|
raise Invalid(
|
||||||
|
"This xiaomi_bslamp2 component requires at least ESPHome "
|
||||||
|
+ "version 2026.4.0; Please upgrade ESPHome and try again."
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
check_version_compatibility,
|
||||||
|
cv.COMPONENT_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
# RGBWW Light
|
||||||
|
cv.Required(CONF_LIGHT): cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_LIGHT_HAL_ID): cv.declare_id(LightHAL),
|
||||||
|
cv.Required(CONF_RED): cv.use_id(LEDCOutput),
|
||||||
|
cv.Required(CONF_GREEN): cv.use_id(LEDCOutput),
|
||||||
|
cv.Required(CONF_BLUE): cv.use_id(LEDCOutput),
|
||||||
|
cv.Required(CONF_WHITE): cv.use_id(LEDCOutput),
|
||||||
|
cv.Required(CONF_MASTER1): cv.use_id(GPIOBinaryOutput),
|
||||||
|
cv.Required(CONF_MASTER2): cv.use_id(GPIOBinaryOutput),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
# Front panel I2C
|
||||||
|
cv.Required(CONF_FRONT_PANEL): cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_FRONT_PANEL_HAL_ID): cv.declare_id(FrontPanelHAL),
|
||||||
|
cv.Required(CONF_I2C): cv.use_id(I2CBus),
|
||||||
|
cv.Required(CONF_ADDRESS): cv.i2c_address,
|
||||||
|
cv.Required(CONF_TRIGGER_PIN): cv.All(pins.internal_gpio_input_pin_schema),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def make_light_hal(config):
|
||||||
|
light_hal = cg.new_Pvariable(config[CONF_LIGHT][CONF_LIGHT_HAL_ID])
|
||||||
|
await cg.register_component(light_hal, config)
|
||||||
|
cg.add(light_hal.set_red_pin(await cg.get_variable(config[CONF_LIGHT][CONF_RED])))
|
||||||
|
cg.add(light_hal.set_green_pin(await cg.get_variable(config[CONF_LIGHT][CONF_GREEN])))
|
||||||
|
cg.add(light_hal.set_blue_pin(await cg.get_variable(config[CONF_LIGHT][CONF_BLUE])))
|
||||||
|
cg.add(light_hal.set_white_pin(await cg.get_variable(config[CONF_LIGHT][CONF_WHITE])))
|
||||||
|
cg.add(light_hal.set_master1_pin(await cg.get_variable(config[CONF_LIGHT][CONF_MASTER1])))
|
||||||
|
cg.add(light_hal.set_master2_pin(await cg.get_variable(config[CONF_LIGHT][CONF_MASTER2])))
|
||||||
|
|
||||||
|
|
||||||
|
async def make_front_panel_hal(config):
|
||||||
|
fp_hal = cg.new_Pvariable(config[CONF_FRONT_PANEL][CONF_FRONT_PANEL_HAL_ID])
|
||||||
|
await cg.register_component(fp_hal, config)
|
||||||
|
trigger_pin = await cg.gpio_pin_expression(config[CONF_FRONT_PANEL][CONF_TRIGGER_PIN])
|
||||||
|
cg.add(fp_hal.set_trigger_pin(trigger_pin))
|
||||||
|
|
||||||
|
fp_i2c_var = await cg.get_variable(config[CONF_FRONT_PANEL][CONF_I2C])
|
||||||
|
cg.add(fp_hal.set_i2c_bus(fp_i2c_var))
|
||||||
|
cg.add(fp_hal.set_i2c_address(config[CONF_FRONT_PANEL][CONF_ADDRESS]))
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
await make_light_hal(config)
|
||||||
|
await make_front_panel_hal(config)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.components import binary_sensor
|
||||||
|
from esphome.const import CONF_ID, CONF_FOR
|
||||||
|
from .. import bslamp2_ns, CODEOWNERS, CONF_FRONT_PANEL_HAL_ID, FrontPanelHAL
|
||||||
|
|
||||||
|
__all__ = ["CODEOWNERS"]
|
||||||
|
|
||||||
|
DEPENDENCIES = ["xiaomi_bslamp2"]
|
||||||
|
|
||||||
|
CONF_PART = "part"
|
||||||
|
|
||||||
|
# The identifier values match the bit values of the events as defined
|
||||||
|
# in ../front_panel_hal.h.
|
||||||
|
PARTS = {
|
||||||
|
"POWER_BUTTON": 0b001 << 1,
|
||||||
|
"POWER": 0b001 << 1,
|
||||||
|
"COLOR_BUTTON": 0b010 << 1,
|
||||||
|
"COLOR": 0b010 << 1,
|
||||||
|
"SLIDER": 0b100 << 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
XiaomiBslamp2TouchBinarySensor = bslamp2_ns.class_(
|
||||||
|
"XiaomiBslamp2TouchBinarySensor", binary_sensor.BinarySensor, cg.Component
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_for(value):
|
||||||
|
value = cv.string(value)
|
||||||
|
return cv.enum(PARTS, upper=True, space="_")(value)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_binary_sensor(conf):
|
||||||
|
if CONF_PART in conf and CONF_FOR in conf:
|
||||||
|
raise cv.Invalid("Specify only one of [part] or [for]")
|
||||||
|
if CONF_PART in conf and CONF_FOR not in conf:
|
||||||
|
# Backward compatibility.
|
||||||
|
conf[CONF_FOR] = conf[CONF_PART]
|
||||||
|
if CONF_FOR not in conf:
|
||||||
|
raise cv.Invalid("'for' is a required option for [binary_sensor.xiaomi_bslamp2]")
|
||||||
|
return conf
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
binary_sensor.binary_sensor_schema()
|
||||||
|
.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(XiaomiBslamp2TouchBinarySensor),
|
||||||
|
cv.GenerateID(CONF_FRONT_PANEL_HAL_ID): cv.use_id(FrontPanelHAL),
|
||||||
|
# This option is not advertised in the documentation. It must be
|
||||||
|
# considered deprecated. I'm not announcing it as such yet. Not
|
||||||
|
# sure if it's useful to do so.
|
||||||
|
cv.Optional(CONF_PART): validate_for,
|
||||||
|
cv.Optional(CONF_FOR): validate_for,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.extend(cv.COMPONENT_SCHEMA),
|
||||||
|
validate_binary_sensor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
yield cg.register_component(var, config)
|
||||||
|
yield binary_sensor.register_binary_sensor(var, config)
|
||||||
|
|
||||||
|
front_panel_hal_var = yield cg.get_variable(config[CONF_FRONT_PANEL_HAL_ID])
|
||||||
|
cg.add(var.set_parent(front_panel_hal_var))
|
||||||
|
cg.add(var.set_for(config[CONF_FOR]))
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../common.h"
|
||||||
|
#include "../front_panel_hal.h"
|
||||||
|
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
namespace bslamp2 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class implements a binary sensor for the touch buttons
|
||||||
|
* and touch slider on the front panel of the Xiaomi Mijia Bedside Lamp 2.
|
||||||
|
*/
|
||||||
|
class XiaomiBslamp2TouchBinarySensor : public binary_sensor::BinarySensor, public Component {
|
||||||
|
public:
|
||||||
|
void set_parent(FrontPanelHAL *front_panel) { front_panel_ = front_panel; }
|
||||||
|
|
||||||
|
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 dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "Front panel binary_sensor:");
|
||||||
|
ESP_LOGCONFIG(TAG, " For: %s", format_part());
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
namespace bslamp2 {
|
||||||
|
|
||||||
|
// Used for logging purposes.
|
||||||
|
static const char *TAG = "xiaomi_bslamp2";
|
||||||
|
|
||||||
|
} // namespace bslamp2
|
||||||
|
} // namespace xiaomi
|
||||||
|
} // namespace esphome
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
#include "esphome/components/i2c/i2c.h"
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
namespace bslamp2 {
|
||||||
|
|
||||||
|
static const uint8_t MSG_LEN = 7;
|
||||||
|
using MSG = uint8_t[MSG_LEN];
|
||||||
|
using LED = uint16_t;
|
||||||
|
using EVENT = uint16_t;
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
// Bit flags that are used for indicating the LEDs in the front panel.
|
||||||
|
// LED_1 is the slider LED closest to the power button.
|
||||||
|
// LED_10 is the one closest to the color button.
|
||||||
|
enum FrontPanelLEDs {
|
||||||
|
LED_ALL = 16384 + 4096 + 1023,
|
||||||
|
LED_ALL_SLIDER = 512 + 256 + 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1,
|
||||||
|
LED_POWER = 16384,
|
||||||
|
LED_COLOR = 4096,
|
||||||
|
LED_1 = 512,
|
||||||
|
LED_2 = 256,
|
||||||
|
LED_3 = 128,
|
||||||
|
LED_4 = 64,
|
||||||
|
LED_5 = 32,
|
||||||
|
LED_6 = 16,
|
||||||
|
LED_7 = 8,
|
||||||
|
LED_8 = 4,
|
||||||
|
LED_9 = 2,
|
||||||
|
LED_10 = 1,
|
||||||
|
LED_NONE = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// This I2C command is used during front panel event handling.
|
||||||
|
static const MSG READY_FOR_EV = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01};
|
||||||
|
|
||||||
|
// Bit flags that are used for specifying an event.
|
||||||
|
// Events are registered using the following bit pattern
|
||||||
|
// (bit 1 being the least significant bit):
|
||||||
|
//
|
||||||
|
// BITS INDICATE PATTERN RESULT
|
||||||
|
// 1 status 0 parsing event failed
|
||||||
|
// 1 parsing event successful
|
||||||
|
// 2-4 part 000 part unknown
|
||||||
|
// 001 power button
|
||||||
|
// 010 color button
|
||||||
|
// 100 slider
|
||||||
|
// 5-6 type 00 type unknown
|
||||||
|
// 01 touch
|
||||||
|
// 10 release
|
||||||
|
// 7-11 slider 00000 level known (or part is not "slider")
|
||||||
|
// level 00001 level 1
|
||||||
|
// ... up to
|
||||||
|
// 10101 level 21
|
||||||
|
//
|
||||||
|
static const EVENT FLAG_INIT = 0b00000000000;
|
||||||
|
|
||||||
|
static const EVENT FLAG_ERR = 0b00000000000;
|
||||||
|
static const EVENT FLAG_OK = 0b00000000001;
|
||||||
|
|
||||||
|
static const EVENT FLAG_PART_SHIFT = 1;
|
||||||
|
static const EVENT FLAG_PART_MASK = 0b00000001110;
|
||||||
|
static const EVENT FLAG_PART_UNKNOWN = 0b00000000000;
|
||||||
|
static const EVENT FLAG_PART_POWER = 0b00000000010;
|
||||||
|
static const EVENT FLAG_PART_COLOR = 0b00000000100;
|
||||||
|
static const EVENT FLAG_PART_SLIDER = 0b00000001000;
|
||||||
|
|
||||||
|
static const EVENT FLAG_TYPE_SHIFT = 4;
|
||||||
|
static const EVENT FLAG_TYPE_MASK = 0b00000110000;
|
||||||
|
static const EVENT FLAG_TYPE_UNKNOWN = 0b00000000000;
|
||||||
|
static const EVENT FLAG_TYPE_TOUCH = 0b00000010000;
|
||||||
|
static const EVENT FLAG_TYPE_RELEASE = 0b00000100000;
|
||||||
|
|
||||||
|
static const EVENT FLAG_LEVEL_SHIFT = 6;
|
||||||
|
static const EVENT FLAG_LEVEL_MASK = 0b11111000000;
|
||||||
|
static const EVENT FLAG_LEVEL_UNKNOWN = 0b00000000000;
|
||||||
|
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class implements a parser that translates event byte codes from the
|
||||||
|
* Xiaomi Mijia Bedside Lamp 2 into usable events.
|
||||||
|
*/
|
||||||
|
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 this->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 this->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 this->error_(ev, m, "invalid slider level crc");
|
||||||
|
else if (m[5] > 0x16 || m[5] < 0x01)
|
||||||
|
return this->error_(ev, m, "out of bounds slider value");
|
||||||
|
else {
|
||||||
|
auto level = 0x17 - m[5];
|
||||||
|
ev |= (level << FLAG_LEVEL_SHIFT);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return this->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;
|
||||||
|
|
||||||
|
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", this->format_part_(ev));
|
||||||
|
ESP_LOGE(TAG, " Parsed event type: %s", this->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";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FrontPanelTriggerStore {
|
||||||
|
volatile int event_id{0};
|
||||||
|
static void gpio_intr(FrontPanelTriggerStore *store);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This ISR is used to handle IRQ triggers from the front panel.
|
||||||
|
*
|
||||||
|
* The front panel pulls the trigger pin low for a short period of time
|
||||||
|
* when a new event is available. All we do here to handle the interrupt,
|
||||||
|
* is increment a simple event id counter. The main loop of the component
|
||||||
|
* will take care of actually reading and processing the event.
|
||||||
|
*/
|
||||||
|
void IRAM_ATTR HOT FrontPanelTriggerStore::gpio_intr(FrontPanelTriggerStore *store) {
|
||||||
|
store->event_id = store->event_id + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a hardware abstraction layer that communicates with with front
|
||||||
|
* panel of the Xiaomi Mijia Bedside Lamp 2.
|
||||||
|
*
|
||||||
|
* It serves as a hub component for other components that implement
|
||||||
|
* the actual buttons and slider components.
|
||||||
|
*/
|
||||||
|
class FrontPanelHAL : public Component, public i2c::I2CDevice {
|
||||||
|
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(InternalGPIOPin *pin) {
|
||||||
|
trigger_pin_ = pin;
|
||||||
|
}
|
||||||
|
|
||||||
|
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...");
|
||||||
|
this->trigger_pin_->setup();
|
||||||
|
this->trigger_pin_->attach_interrupt(
|
||||||
|
FrontPanelTriggerStore::gpio_intr,
|
||||||
|
&this->store_,
|
||||||
|
gpio::INTERRUPT_FALLING_EDGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "FrontPanelHAL:");
|
||||||
|
LOG_I2C_DEVICE(this);
|
||||||
|
LOG_PIN(" I2C interrupt pin: ", trigger_pin_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
// Read and publish front panel events.
|
||||||
|
auto current_event_id = this->store_.event_id;
|
||||||
|
if (current_event_id != this->last_event_id_) {
|
||||||
|
this->last_event_id_ = current_event_id;
|
||||||
|
if (this->write(READY_FOR_EV, MSG_LEN) != i2c::ERROR_OK) {
|
||||||
|
ESP_LOGW(TAG, "Writing READY_FOR_EV to front panel failed");
|
||||||
|
}
|
||||||
|
MSG message;
|
||||||
|
if (this->read(message, MSG_LEN) != i2c::ERROR_OK) {
|
||||||
|
ESP_LOGW(TAG, "Reading message from front panel failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto ev = event.parse(message);
|
||||||
|
if (ev & FLAG_OK) {
|
||||||
|
this->event_callback_.call(ev);
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "Skipping unsupported message from front panel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (led_state_ != last_led_state_) {
|
||||||
|
update_leds();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn on one or more LEDs (leaving the state of the other LEDs intact).
|
||||||
|
* The input value is a bitwise OR-ed set of LED constants.
|
||||||
|
* Only after a call to update_leds() (handled by default from the main loop),
|
||||||
|
* the new state will be activated.
|
||||||
|
*/
|
||||||
|
void turn_on_leds(uint16_t leds) {
|
||||||
|
led_state_ = led_state_ | 0b0000110000000000 | leds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn off one or more LEDs (leaving the state of the other LEDs intact).
|
||||||
|
* The input value is a bitwise OR-ed set of LED constants.
|
||||||
|
* Only after a call to update_leds() (handled by default from the main loop),
|
||||||
|
* the new state will be activated.
|
||||||
|
*/
|
||||||
|
void turn_off_leds(uint16_t leds) {
|
||||||
|
led_state_ = (led_state_ | 0b0000110000000000) & ~leds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the state of the LEDs according to the provided input.
|
||||||
|
* The input value is a bitwise OR-ed set of LED constants, representing the
|
||||||
|
* LEDs that must be turned on. All other LEDs are turned off.
|
||||||
|
* Only after a call to update_leds() (handled by default from the main loop),
|
||||||
|
* the new state will be activated.
|
||||||
|
*/
|
||||||
|
void set_leds(uint16_t leds) {
|
||||||
|
turn_off_leds(LED_ALL);
|
||||||
|
turn_on_leds(leds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate the LEDs according to the currently stored LED state. This method
|
||||||
|
* will be called automatically by the main loop. You can call this method,
|
||||||
|
* in case you need to update the LED state right away.
|
||||||
|
*/
|
||||||
|
void update_leds() {
|
||||||
|
led_msg_[2] = led_state_ >> 8;
|
||||||
|
led_msg_[3] = led_state_ & 0xff;
|
||||||
|
write(led_msg_, MSG_LEN);
|
||||||
|
last_led_state_ = led_state_;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the front panel slider illumination to the provided level,
|
||||||
|
* based on a float input (0.0 - 1.0).
|
||||||
|
*
|
||||||
|
* This implements the behavior of the original firmware for representing
|
||||||
|
* the lamp's brightness.
|
||||||
|
*
|
||||||
|
* Level 0.0 means: turn off the slider illumination.
|
||||||
|
* The other levels are translated to one of the available levels.
|
||||||
|
*/
|
||||||
|
void set_slider_level(float level) {
|
||||||
|
turn_off_leds(LED_ALL_SLIDER);
|
||||||
|
if (level == 0.00f) return;
|
||||||
|
if (level > 0.00f) turn_on_leds(LED_1);
|
||||||
|
if (level > 0.15f) turn_on_leds(LED_2);
|
||||||
|
if (level > 0.25f) turn_on_leds(LED_3);
|
||||||
|
if (level > 0.35f) turn_on_leds(LED_4);
|
||||||
|
if (level > 0.45f) turn_on_leds(LED_5);
|
||||||
|
if (level > 0.55f) turn_on_leds(LED_6);
|
||||||
|
if (level > 0.65f) turn_on_leds(LED_7);
|
||||||
|
if (level > 0.75f) turn_on_leds(LED_8);
|
||||||
|
if (level > 0.85f) turn_on_leds(LED_9);
|
||||||
|
if (level > 0.95f) turn_on_leds(LED_10);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
InternalGPIOPin *trigger_pin_;
|
||||||
|
FrontPanelTriggerStore store_{};
|
||||||
|
int last_event_id_ = 0;
|
||||||
|
CallbackManager<void(EVENT)> event_callback_{};
|
||||||
|
|
||||||
|
uint16_t led_state_ = 0;
|
||||||
|
uint16_t last_led_state_ = 0;
|
||||||
|
MSG led_msg_ = {0x02, 0x03, 0x00, 0x00, 0x64, 0x00, 0x00};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace bslamp2
|
||||||
|
} // namespace xiaomi
|
||||||
|
} // namespace esphome
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.components import light
|
||||||
|
from esphome import automation
|
||||||
|
from esphome.core import Lambda
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_RED,
|
||||||
|
CONF_GREEN,
|
||||||
|
CONF_BLUE,
|
||||||
|
CONF_COLOR_TEMPERATURE,
|
||||||
|
CONF_STATE,
|
||||||
|
CONF_OUTPUT_ID,
|
||||||
|
CONF_TRIGGER_ID,
|
||||||
|
CONF_ID,
|
||||||
|
CONF_TRANSITION_LENGTH,
|
||||||
|
CONF_BRIGHTNESS,
|
||||||
|
CONF_EFFECT,
|
||||||
|
CONF_FLASH_LENGTH,
|
||||||
|
CONF_RESTORE_MODE,
|
||||||
|
)
|
||||||
|
from .. import bslamp2_ns, CODEOWNERS, CONF_LIGHT_HAL_ID, LightHAL
|
||||||
|
|
||||||
|
__all__ = ["CODEOWNERS"]
|
||||||
|
|
||||||
|
DEPENDENCIES = ["xiaomi_bslamp2"]
|
||||||
|
|
||||||
|
CONF_ON_BRIGHTNESS = "on_brightness"
|
||||||
|
CONF_PRESET_ID = "preset_id"
|
||||||
|
CONF_PRESETS_ID = "presets_id"
|
||||||
|
CONF_PRESET = "preset"
|
||||||
|
CONF_PRESETS = "presets"
|
||||||
|
CONF_NEXT = "next"
|
||||||
|
CONF_GROUP = "group"
|
||||||
|
CONF_NIGHT_CALIBRATION = "night_calibration"
|
||||||
|
|
||||||
|
MIRED_MIN = 153
|
||||||
|
MIRED_MAX = 588
|
||||||
|
DEFAULT_NIGHT_LIGHT_RED = 0.968
|
||||||
|
DEFAULT_NIGHT_LIGHT_GREEN = 0.968
|
||||||
|
DEFAULT_NIGHT_LIGHT_BLUE = 0.972
|
||||||
|
|
||||||
|
XiaomiBslamp2LightState = bslamp2_ns.class_("XiaomiBslamp2LightState", light.LightState)
|
||||||
|
XiaomiBslamp2LightOutput = bslamp2_ns.class_("XiaomiBslamp2LightOutput", light.LightOutput)
|
||||||
|
PresetsContainer = bslamp2_ns.class_("PresetsContainer", cg.Component)
|
||||||
|
Preset = bslamp2_ns.class_("Preset", cg.Component)
|
||||||
|
BrightnessTrigger = bslamp2_ns.class_("BrightnessTrigger", automation.Trigger.template())
|
||||||
|
ActivatePresetAction = bslamp2_ns.class_("ActivatePresetAction", automation.Action)
|
||||||
|
DiscoAction = bslamp2_ns.class_("DiscoAction", automation.Action)
|
||||||
|
|
||||||
|
PRESETS_SCHEMA = cv.Schema({str.lower: cv.Schema({str.lower: light.automation.LIGHT_TURN_ON_ACTION_SCHEMA})})
|
||||||
|
|
||||||
|
|
||||||
|
def validate_preset(config):
|
||||||
|
has_rgb = CONF_RED in config or CONF_GREEN in config or CONF_BLUE in config
|
||||||
|
has_white = CONF_COLOR_TEMPERATURE in config
|
||||||
|
has_effect = CONF_EFFECT in config
|
||||||
|
|
||||||
|
# Check mutual exclusivity of preset options.
|
||||||
|
if (has_rgb + has_white + has_effect) > 1:
|
||||||
|
raise cv.Invalid("Use only one of RGB light, white (color temperature) light or an effect")
|
||||||
|
|
||||||
|
# Check the color temperature value range.
|
||||||
|
if has_white:
|
||||||
|
if config[CONF_COLOR_TEMPERATURE] < MIRED_MIN or config[CONF_COLOR_TEMPERATURE] > MIRED_MAX:
|
||||||
|
raise cv.Invalid(f"The color temperature must be in the range {MIRED_MIN} - {MIRED_MAX}")
|
||||||
|
|
||||||
|
# When defining an RGB color, it is allowed to omit RGB components that have value 0.
|
||||||
|
if has_rgb:
|
||||||
|
if CONF_RED not in config:
|
||||||
|
config[CONF_RED] = 0
|
||||||
|
if CONF_GREEN not in config:
|
||||||
|
config[CONF_GREEN] = 0
|
||||||
|
if CONF_BLUE not in config:
|
||||||
|
config[CONF_BLUE] = 0
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
PRESET_SCHEMA = cv.All(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_ID): cv.use_id(XiaomiBslamp2LightState),
|
||||||
|
cv.GenerateID(CONF_PRESET_ID): cv.declare_id(Preset),
|
||||||
|
cv.Optional(CONF_EFFECT): cv.string,
|
||||||
|
cv.Optional(CONF_COLOR_TEMPERATURE): cv.color_temperature,
|
||||||
|
cv.Optional(CONF_RED): cv.percentage,
|
||||||
|
cv.Optional(CONF_GREEN): cv.percentage,
|
||||||
|
cv.Optional(CONF_BLUE): cv.percentage,
|
||||||
|
cv.Optional(CONF_BRIGHTNESS): cv.percentage,
|
||||||
|
cv.Optional(CONF_TRANSITION_LENGTH): cv.positive_time_period_milliseconds,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
validate_preset,
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_ID): cv.declare_id(XiaomiBslamp2LightState),
|
||||||
|
cv.GenerateID(CONF_LIGHT_HAL_ID): cv.use_id(LightHAL),
|
||||||
|
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(XiaomiBslamp2LightOutput),
|
||||||
|
cv.Optional(CONF_RESTORE_MODE, default="RESTORE_DEFAULT_OFF"): cv.enum(
|
||||||
|
light.RESTORE_MODES, upper=True, space="_"
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_NIGHT_CALIBRATION, default={}): cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_RED, default=DEFAULT_NIGHT_LIGHT_RED): cv.float_range(min=0.0, max=1.0),
|
||||||
|
cv.Optional(CONF_GREEN, default=DEFAULT_NIGHT_LIGHT_GREEN): cv.float_range(min=0.0, max=1.0),
|
||||||
|
cv.Optional(CONF_BLUE, default=DEFAULT_NIGHT_LIGHT_BLUE): cv.float_range(min=0.0, max=1.0),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_ON_BRIGHTNESS): automation.validate_automation(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BrightnessTrigger),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
cv.GenerateID(CONF_PRESETS_ID): cv.declare_id(PresetsContainer),
|
||||||
|
cv.Optional(CONF_PRESETS): cv.Schema({str.lower: cv.Schema({str.lower: PRESET_SCHEMA})}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_preset_group(value):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def is_preset(value):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_simple_preset_action(schema):
|
||||||
|
def validator(value):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return schema(value)
|
||||||
|
value = value.lower()
|
||||||
|
config = {}
|
||||||
|
if value == "next_group":
|
||||||
|
config[CONF_NEXT] = CONF_GROUP
|
||||||
|
elif value == "next_preset":
|
||||||
|
config[CONF_NEXT] = CONF_PRESET
|
||||||
|
elif "." not in value:
|
||||||
|
config[CONF_GROUP] = value
|
||||||
|
else:
|
||||||
|
group, preset = value.split(".", 2)
|
||||||
|
config[CONF_GROUP] = group
|
||||||
|
config[CONF_PRESET] = preset
|
||||||
|
return schema(config)
|
||||||
|
|
||||||
|
return validator
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"light.disco_on",
|
||||||
|
DiscoAction,
|
||||||
|
light.automation.LIGHT_TURN_ON_ACTION_SCHEMA,
|
||||||
|
synchronous=True,
|
||||||
|
)
|
||||||
|
def disco_action_on_to_code(config, action_id, template_arg, args):
|
||||||
|
light_var = yield cg.get_variable(config[CONF_ID])
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg, light_var)
|
||||||
|
|
||||||
|
if CONF_STATE in config:
|
||||||
|
template_ = yield cg.templatable(config[CONF_STATE], args, bool)
|
||||||
|
cg.add(var.set_state(template_))
|
||||||
|
if CONF_TRANSITION_LENGTH in config:
|
||||||
|
template_ = yield cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32)
|
||||||
|
cg.add(var.set_transition_length(template_))
|
||||||
|
if CONF_FLASH_LENGTH in config:
|
||||||
|
template_ = yield cg.templatable(config[CONF_FLASH_LENGTH], args, cg.uint32)
|
||||||
|
cg.add(var.set_flash_length(template_))
|
||||||
|
if CONF_BRIGHTNESS in config:
|
||||||
|
template_ = yield cg.templatable(config[CONF_BRIGHTNESS], args, float)
|
||||||
|
cg.add(var.set_brightness(template_))
|
||||||
|
if CONF_RED in config:
|
||||||
|
template_ = yield cg.templatable(config[CONF_RED], args, float)
|
||||||
|
cg.add(var.set_red(template_))
|
||||||
|
if CONF_GREEN in config:
|
||||||
|
template_ = yield cg.templatable(config[CONF_GREEN], args, float)
|
||||||
|
cg.add(var.set_green(template_))
|
||||||
|
if CONF_BLUE in config:
|
||||||
|
template_ = yield cg.templatable(config[CONF_BLUE], args, float)
|
||||||
|
cg.add(var.set_blue(template_))
|
||||||
|
if CONF_COLOR_TEMPERATURE in config:
|
||||||
|
template_ = yield cg.templatable(config[CONF_COLOR_TEMPERATURE], args, float)
|
||||||
|
cg.add(var.set_color_temperature(template_))
|
||||||
|
if CONF_EFFECT in config:
|
||||||
|
template_ = yield cg.templatable(config[CONF_EFFECT], args, cg.std_string)
|
||||||
|
cg.add(var.set_effect(template_))
|
||||||
|
yield var
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"light.disco_off",
|
||||||
|
DiscoAction,
|
||||||
|
light.automation.LIGHT_TURN_OFF_ACTION_SCHEMA,
|
||||||
|
synchronous=True,
|
||||||
|
)
|
||||||
|
async def disco_action_off_to_code(config, action_id, template_arg, args):
|
||||||
|
light_var = await cg.get_variable(config[CONF_ID])
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg, light_var)
|
||||||
|
template_ = await cg.templatable(False, args, bool)
|
||||||
|
cg.add(var.set_disco_state(template_))
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
USED_PRESETS = []
|
||||||
|
|
||||||
|
|
||||||
|
def register_preset_action(value):
|
||||||
|
if "group" in value and not isinstance(value["group"], Lambda):
|
||||||
|
if "preset" in value and not isinstance(value["preset"], Lambda):
|
||||||
|
preset_data = [value["group"], value["preset"]]
|
||||||
|
else:
|
||||||
|
preset_data = [value["group"], None]
|
||||||
|
USED_PRESETS.append(preset_data)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"preset.activate",
|
||||||
|
ActivatePresetAction,
|
||||||
|
cv.All(
|
||||||
|
maybe_simple_preset_action(
|
||||||
|
cv.Any(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_PRESETS_ID): cv.use_id(PresetsContainer),
|
||||||
|
cv.Required(CONF_GROUP): cv.templatable(cv.string),
|
||||||
|
cv.Optional(CONF_PRESET): cv.templatable(cv.string),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_PRESETS_ID): cv.use_id(PresetsContainer),
|
||||||
|
cv.Required(CONF_NEXT): cv.one_of(CONF_GROUP, CONF_PRESET, lower=True),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
register_preset_action,
|
||||||
|
),
|
||||||
|
synchronous=True,
|
||||||
|
)
|
||||||
|
def preset_activate_to_code(config, action_id, template_arg, args):
|
||||||
|
presets_var = yield cg.get_variable(config[CONF_PRESETS_ID])
|
||||||
|
action_var = cg.new_Pvariable(action_id, template_arg, presets_var)
|
||||||
|
if CONF_NEXT in config:
|
||||||
|
cg.add(action_var.set_operation(f"next_{config[CONF_NEXT]}"))
|
||||||
|
elif CONF_PRESET in config:
|
||||||
|
cg.add(action_var.set_operation("activate_preset"))
|
||||||
|
group_template_ = yield cg.templatable(config[CONF_GROUP], args, cg.std_string)
|
||||||
|
cg.add(action_var.set_group(group_template_))
|
||||||
|
preset_template_ = yield cg.templatable(config[CONF_PRESET], args, cg.std_string)
|
||||||
|
cg.add(action_var.set_preset(preset_template_))
|
||||||
|
else:
|
||||||
|
cg.add(action_var.set_operation("activate_group"))
|
||||||
|
group_template_ = yield cg.templatable(config[CONF_GROUP], args, cg.std_string)
|
||||||
|
cg.add(action_var.set_group(group_template_))
|
||||||
|
yield action_var
|
||||||
|
|
||||||
|
|
||||||
|
async def light_output_to_code(config):
|
||||||
|
light_output_var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
|
||||||
|
await light.register_light(light_output_var, config)
|
||||||
|
light_hal_var = await cg.get_variable(config[CONF_LIGHT_HAL_ID])
|
||||||
|
cg.add(light_output_var.set_parent(light_hal_var))
|
||||||
|
night_calibration = config[CONF_NIGHT_CALIBRATION]
|
||||||
|
cg.add(
|
||||||
|
light_output_var.set_night_light_color_temperature_calibration(
|
||||||
|
night_calibration[CONF_RED],
|
||||||
|
night_calibration[CONF_GREEN],
|
||||||
|
night_calibration[CONF_BLUE],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_brightness_to_code(config):
|
||||||
|
light_output_var = await cg.get_variable(config[CONF_OUTPUT_ID])
|
||||||
|
for config_ in config.get(CONF_ON_BRIGHTNESS, []):
|
||||||
|
trigger = cg.new_Pvariable(config_[CONF_TRIGGER_ID], light_output_var)
|
||||||
|
await automation.build_automation(trigger, [(float, "x")], config_)
|
||||||
|
|
||||||
|
|
||||||
|
async def preset_to_code(config, preset_group, preset_name):
|
||||||
|
light_var = await cg.get_variable(config[CONF_ID])
|
||||||
|
preset_var = cg.new_Pvariable(config[CONF_PRESET_ID], light_var, preset_group, preset_name)
|
||||||
|
if CONF_TRANSITION_LENGTH in config:
|
||||||
|
cg.add(preset_var.set_transition_length(config[CONF_TRANSITION_LENGTH]))
|
||||||
|
if CONF_BRIGHTNESS in config:
|
||||||
|
cg.add(preset_var.set_brightness(config[CONF_BRIGHTNESS]))
|
||||||
|
if CONF_RED in config:
|
||||||
|
cg.add(preset_var.set_red(config[CONF_RED]))
|
||||||
|
if CONF_GREEN in config:
|
||||||
|
cg.add(preset_var.set_green(config[CONF_GREEN]))
|
||||||
|
if CONF_BLUE in config:
|
||||||
|
cg.add(preset_var.set_blue(config[CONF_BLUE]))
|
||||||
|
if CONF_COLOR_TEMPERATURE in config:
|
||||||
|
cg.add(preset_var.set_color_temperature(config[CONF_COLOR_TEMPERATURE]))
|
||||||
|
if CONF_EFFECT in config:
|
||||||
|
cg.add(preset_var.set_effect(config[CONF_EFFECT]))
|
||||||
|
else:
|
||||||
|
cg.add(preset_var.set_effect("None"))
|
||||||
|
return await cg.register_component(preset_var, config)
|
||||||
|
|
||||||
|
|
||||||
|
async def presets_to_code(config):
|
||||||
|
presets_var = cg.new_Pvariable(config[CONF_PRESETS_ID])
|
||||||
|
await cg.register_component(presets_var, config)
|
||||||
|
|
||||||
|
for preset_group, presets in config.get(CONF_PRESETS, {}).items():
|
||||||
|
for preset_name, preset_config in presets.items():
|
||||||
|
preset = await preset_to_code(preset_config, preset_group, preset_name)
|
||||||
|
cg.add(presets_var.add_preset(preset))
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
await light_output_to_code(config)
|
||||||
|
await on_brightness_to_code(config)
|
||||||
|
await presets_to_code(config)
|
||||||
|
|
||||||
|
|
||||||
|
def validate(config):
|
||||||
|
valid_presets = config.get(CONF_PRESETS, {})
|
||||||
|
for group, preset in USED_PRESETS:
|
||||||
|
if group not in valid_presets:
|
||||||
|
raise cv.Invalid(f"Invalid light preset group '{group}' used")
|
||||||
|
if preset is not None and preset not in valid_presets[group]:
|
||||||
|
raise cv.Invalid(f"Invalid light preset '{group}.{preset}' used")
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
FINAL_VALIDATE_SCHEMA = cv.Schema(validate)
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "interfaces.h"
|
||||||
|
#include "light_output.h"
|
||||||
|
#include "light_state.h"
|
||||||
|
#include "presets.h"
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
namespace bslamp2 {
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class DiscoAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
explicit DiscoAction(LightStateDiscoSupport *parent) : parent_(parent) {}
|
||||||
|
|
||||||
|
TEMPLATABLE_VALUE(bool, disco_state)
|
||||||
|
TEMPLATABLE_VALUE(bool, state)
|
||||||
|
TEMPLATABLE_VALUE(uint32_t, transition_length)
|
||||||
|
TEMPLATABLE_VALUE(uint32_t, flash_length)
|
||||||
|
TEMPLATABLE_VALUE(float, brightness)
|
||||||
|
TEMPLATABLE_VALUE(float, red)
|
||||||
|
TEMPLATABLE_VALUE(float, green)
|
||||||
|
TEMPLATABLE_VALUE(float, blue)
|
||||||
|
TEMPLATABLE_VALUE(float, color_temperature)
|
||||||
|
TEMPLATABLE_VALUE(std::string, effect)
|
||||||
|
|
||||||
|
void play(const Ts &...x) override {
|
||||||
|
if (this->disco_state_.has_value()) {
|
||||||
|
auto p = this->disco_state_.optional_value(x...);
|
||||||
|
if (!*p) {
|
||||||
|
parent_->disco_stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto call = this->parent_->make_disco_call(false);
|
||||||
|
call.set_state(this->state_.optional_value(x...));
|
||||||
|
call.set_brightness(this->brightness_.optional_value(x...));
|
||||||
|
call.set_red(this->red_.optional_value(x...));
|
||||||
|
call.set_green(this->green_.optional_value(x...));
|
||||||
|
call.set_blue(this->blue_.optional_value(x...));
|
||||||
|
call.set_color_temperature(this->color_temperature_.optional_value(x...));
|
||||||
|
call.set_effect(this->effect_.optional_value(x...));
|
||||||
|
call.set_flash_length(this->flash_length_.optional_value(x...));
|
||||||
|
call.set_transition_length(this->transition_length_.optional_value(x...));
|
||||||
|
|
||||||
|
// Force the light to update right now, not in the next loop.
|
||||||
|
call.perform();
|
||||||
|
parent_->disco_apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
LightStateDiscoSupport *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
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(const 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_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace bslamp2
|
||||||
|
} // namespace xiaomi
|
||||||
|
} // namespace esphome
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../light_hal.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
namespace bslamp2 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This abstract class is used for implementing classes that translate
|
||||||
|
* LightColorValues into the required GPIO PWM duty cycle levels to represent
|
||||||
|
* the requested color on the physical device.
|
||||||
|
*/
|
||||||
|
class ColorHandler : public GPIOOutputValues {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace bslamp2
|
||||||
|
} // namespace xiaomi
|
||||||
|
} // namespace esphome
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include "../common.h"
|
||||||
|
#include "color_handler.h"
|
||||||
|
#include "color_handler_off.h"
|
||||||
|
#include "color_handler_rgb.h"
|
||||||
|
#include "color_handler_color_temperature.h"
|
||||||
|
#include "color_handler_night_light.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
namespace bslamp2 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class translates LightColorValues into GPIO duty cycles that can be
|
||||||
|
* used for representing a requested light color on the physical device.
|
||||||
|
*
|
||||||
|
* The code handles all known light modes for the device:
|
||||||
|
*
|
||||||
|
* - off: the light is off
|
||||||
|
* - night light: based on RGB or white mode + lowest possible brightness
|
||||||
|
* - white light: based on color temperature + brightness
|
||||||
|
* - RGB light: based on RGB values + brightness
|
||||||
|
*/
|
||||||
|
class ColorHandlerChain : public ColorHandler {
|
||||||
|
public:
|
||||||
|
void set_night_light_color_temperature_calibration(NightLightCalibration calibration) {
|
||||||
|
night_light_.set_color_temperature_calibration(calibration);
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_night_light_color_temperature_calibration(float red, float green, float blue) {
|
||||||
|
set_night_light_color_temperature_calibration({red, green, blue});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool set_light_color_values(light::LightColorValues v) {
|
||||||
|
// The actual implementation of the various light modes is in separate
|
||||||
|
// 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 {
|
||||||
|
ESP_LOGE(TAG, "Light color error: (None of the ColorHandler classes handles the requested light state; defaulting to 'off'");
|
||||||
|
off_light_.copy_to(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
ColorHandlerOff off_light_;
|
||||||
|
ColorHandlerRGB rgb_light_;
|
||||||
|
ColorHandlerColorTemperature white_light_;
|
||||||
|
ColorHandlerNightLight night_light_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace bslamp2
|
||||||
|
} // namespace xiaomi
|
||||||
|
} // namespace esphome
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include "../common.h"
|
||||||
|
#include "../light_hal.h"
|
||||||
|
#include "color_handler.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
namespace bslamp2 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimum color temperature in mired.
|
||||||
|
* Same as supported by the original firmware.
|
||||||
|
*/
|
||||||
|
static const int MIRED_MIN = 153;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum color temperature in mired.
|
||||||
|
* Same as supported by the original firmware.
|
||||||
|
*/
|
||||||
|
static const int MIRED_MAX = 588;
|
||||||
|
|
||||||
|
struct RGBWLevelsByTemperature {
|
||||||
|
float from_temperature;
|
||||||
|
float red;
|
||||||
|
float green;
|
||||||
|
float blue;
|
||||||
|
float white;
|
||||||
|
};
|
||||||
|
|
||||||
|
using RGBWLevelsTable = std::array<RGBWLevelsByTemperature, 15>;
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
static const RGBWLevelsTable rgbw_levels_1_ {{
|
||||||
|
{ 501.0f, 0.873f, 0.907f, 1.000f, 0.063f },
|
||||||
|
{ 455.0f, 0.873f, 0.896f, 1.000f, 0.063f },
|
||||||
|
{ 417.0f, 0.873f, 0.891f, 1.000f, 0.068f },
|
||||||
|
{ 371.0f, 0.873f, 0.880f, 1.000f, 0.070f },
|
||||||
|
{ 334.0f, 0.873f, 0.887f, 1.000f, 0.088f },
|
||||||
|
{ 313.0f, 0.882f, 0.904f, 1.000f, 0.128f },
|
||||||
|
{ 295.0f, 0.947f, 1.000f, 0.968f, 0.145f },
|
||||||
|
{ 251.0f, 0.999f, 1.000f, 1.000f, 0.155f },
|
||||||
|
{ 223.0f, 1.000f, 0.899f, 0.921f, 0.130f },
|
||||||
|
{ 201.0f, 1.000f, 0.873f, 0.908f, 0.115f },
|
||||||
|
{ 182.0f, 1.000f, 0.873f, 0.901f, 0.103f },
|
||||||
|
{ 173.0f, 1.000f, 0.873f, 0.904f, 0.094f },
|
||||||
|
{ 167.0f, 1.000f, 0.873f, 0.891f, 0.098f },
|
||||||
|
{ 154.0f, 1.000f, 0.873f, 0.894f, 0.090f },
|
||||||
|
{ 153.0f, 1.000f, 0.873f, 0.892f, 0.088f }
|
||||||
|
}};
|
||||||
|
|
||||||
|
static const RGBWLevelsTable rgbw_levels_100_ {{
|
||||||
|
{ 501.0f, 0.000f, 0.344f, 1.000f, 0.068f },
|
||||||
|
{ 455.0f, 0.000f, 0.237f, 1.000f, 0.093f },
|
||||||
|
{ 417.0f, 0.000f, 0.186f, 1.000f, 0.120f },
|
||||||
|
{ 371.0f, 0.000f, 0.149f, 1.000f, 0.167f },
|
||||||
|
{ 334.0f, 0.000f, 0.135f, 1.000f, 0.325f },
|
||||||
|
{ 313.0f, 0.097f, 0.314f, 1.000f, 0.740f },
|
||||||
|
{ 295.0f, 0.745f, 1.000f, 0.953f, 0.905f },
|
||||||
|
{ 251.0f, 1.000f, 1.000f, 1.000f, 1.000f },
|
||||||
|
{ 223.0f, 1.000f, 0.267f, 0.485f, 0.765f },
|
||||||
|
{ 201.0f, 1.000f, 0.000f, 0.355f, 0.609f },
|
||||||
|
{ 182.0f, 1.000f, 0.000f, 0.282f, 0.489f },
|
||||||
|
{ 173.0f, 1.000f, 0.000f, 0.313f, 0.392f },
|
||||||
|
{ 167.0f, 1.000f, 0.000f, 0.180f, 0.422f },
|
||||||
|
{ 154.0f, 1.000f, 0.000f, 0.218f, 0.368f },
|
||||||
|
{ 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,
|
||||||
|
* based on color temperature + brightness.
|
||||||
|
*/
|
||||||
|
class ColorHandlerColorTemperature : public ColorHandler {
|
||||||
|
public:
|
||||||
|
bool set_light_color_values(light::LightColorValues v) {
|
||||||
|
light_mode = LIGHT_MODE_WHITE;
|
||||||
|
|
||||||
|
if (v.get_color_mode() != light::ColorMode::COLOR_TEMPERATURE) {
|
||||||
|
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 = std::lerp(levels_1.red, levels_100.red, brightness);
|
||||||
|
green = std::lerp(levels_1.green, levels_100.green, brightness);
|
||||||
|
blue = std::lerp(levels_1.blue, levels_100.blue, brightness);
|
||||||
|
white = std::lerp(levels_1.white, levels_100.white, brightness);
|
||||||
|
|
||||||
|
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;
|
||||||
|
// Temperature too low. Shouldn't happen, because of validation
|
||||||
|
// at higher levels. But when it happens, simply return the data
|
||||||
|
// for lowest available temperature.
|
||||||
|
return table[0];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace bslamp2
|
||||||
|
} // namespace xiaomi
|
||||||
|
} // namespace esphome
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../common.h"
|
||||||
|
#include "../light_hal.h"
|
||||||
|
#include "color_handler.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
namespace bslamp2 {
|
||||||
|
|
||||||
|
struct NightLightCalibration {
|
||||||
|
float red;
|
||||||
|
float green;
|
||||||
|
float blue;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const NightLightCalibration DEFAULT_NIGHT_LIGHT_CALIBRATION{0.968f, 0.968f, 0.972f};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class can handle the GPIO outputs for the night light mode.
|
||||||
|
*
|
||||||
|
* At the lowest brightness setting, the light will switch to night light
|
||||||
|
* mode. In the Yeelight integration in Home Assistant, this feature is
|
||||||
|
* exposed trough a separate switch. I have found that the switch is both
|
||||||
|
* confusing and made me run into issues when automating the lights.
|
||||||
|
* Using the lowest brightness for triggering the night light feels a lot
|
||||||
|
* more natural.
|
||||||
|
*
|
||||||
|
* Note that if a switch is still the preferred way to handle the night
|
||||||
|
* light mode toggle, then this still could be implemented through the
|
||||||
|
* device's yaml configuration.
|
||||||
|
*/
|
||||||
|
class ColorHandlerNightLight : public ColorHandler {
|
||||||
|
public:
|
||||||
|
void set_color_temperature_calibration(NightLightCalibration calibration) {
|
||||||
|
color_temperature_calibration_.red = clamp(calibration.red, 0.0f, 1.0f);
|
||||||
|
color_temperature_calibration_.green = clamp(calibration.green, 0.0f, 1.0f);
|
||||||
|
color_temperature_calibration_.blue = clamp(calibration.blue, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_color_temperature_calibration(float red, float green, float blue) {
|
||||||
|
set_color_temperature_calibration({red, green, blue});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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_color_mode() == light::ColorMode::COLOR_TEMPERATURE) {
|
||||||
|
red = color_temperature_calibration_.red;
|
||||||
|
green = color_temperature_calibration_.green;
|
||||||
|
blue = color_temperature_calibration_.blue;
|
||||||
|
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 = std::lerp(0.9997f, 0.9680f, v.get_red());
|
||||||
|
green = std::lerp(0.9997f, 0.9680f, v.get_green());
|
||||||
|
auto blue_scale = (v.get_red() + v.get_green()) / 2.0f;
|
||||||
|
auto blue_max = std::lerp(0.9640f, 0.9720f, blue_scale);
|
||||||
|
blue = std::lerp(0.9997f, blue_max, v.get_blue());
|
||||||
|
white = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
NightLightCalibration color_temperature_calibration_{DEFAULT_NIGHT_LIGHT_CALIBRATION};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace bslamp2
|
||||||
|
} // namespace xiaomi
|
||||||
|
} // namespace esphome
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../common.h"
|
||||||
|
#include "../light_hal.h"
|
||||||
|
#include "color_handler.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
namespace bslamp2 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class can handle the GPIO outputs in case the light is turned off.
|
||||||
|
*/
|
||||||
|
class ColorHandlerOff : public ColorHandler {
|
||||||
|
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;
|
||||||
|
|
||||||
|
red = 1.0f;
|
||||||
|
green = 1.0f;
|
||||||
|
blue = 1.0f;
|
||||||
|
white = 0.0f;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace bslamp2
|
||||||
|
} // namespace xiaomi
|
||||||
|
} // namespace esphome
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "../common.h"
|
||||||
|
#include "../light_hal.h"
|
||||||
|
#include "color_handler.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
namespace bslamp2 {
|
||||||
|
|
||||||
|
struct RGB {
|
||||||
|
float red;
|
||||||
|
float green;
|
||||||
|
float blue;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RGBPoint {
|
||||||
|
RGB low;
|
||||||
|
RGB high;
|
||||||
|
};
|
||||||
|
|
||||||
|
using RGBRing = std::array<RGBPoint, 24>;
|
||||||
|
|
||||||
|
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 base for this table are measurements against the original device
|
||||||
|
* firmware, using the RGB color circle as used in Home Assistant as the
|
||||||
|
* color space model.
|
||||||
|
*
|
||||||
|
* This circle has 7 colored rings around a white center point. The outer
|
||||||
|
* ring, with the highest saturation, is numbered as 0. The inner ring
|
||||||
|
* around the white center point is numbered as 6. The white center point
|
||||||
|
* itself is numbered as 7, although this one cannot really be called "a
|
||||||
|
* ring".
|
||||||
|
*
|
||||||
|
* For each ring, there are 24 color positions, starting at the color red
|
||||||
|
* (0°), going around the circle clockwise via green (120°) and blue (240°).
|
||||||
|
*
|
||||||
|
* For each color position, two duty cycle measurements are registered:
|
||||||
|
* - one defining the duty cycles at 1% brightness
|
||||||
|
* - one defining the duty cycles at 100% brightness Duty cycles for
|
||||||
|
* in-between brightnesses can be derived from these values by means of
|
||||||
|
* linear interpolation.
|
||||||
|
*/
|
||||||
|
// clang-format off
|
||||||
|
static const RGBCircle rgb_circle_ {{
|
||||||
|
// Ring 0, min value RGB component value = 0
|
||||||
|
{{
|
||||||
|
{{ 0.8998, 0.9997, 0.9997 }, { 0.0000, 0.9997, 0.9997 }}, // 0° [255,0,0] (red)
|
||||||
|
{{ 0.8727, 0.9404, 0.9682 }, { 0.0000, 0.6758, 0.9539 }}, // 15° [255,0,63]
|
||||||
|
{{ 0.8727, 0.8967, 0.9677 }, { 0.0000, 0.2389, 0.9506 }}, // 30° [255,0,126]
|
||||||
|
{{ 0.9030, 0.8727, 0.9682 }, { 0.3040, 0.0000, 0.9536 }}, // 45° [255,0,190]
|
||||||
|
{{ 0.9270, 0.8727, 0.9685 }, { 0.5426, 0.0000, 0.9570 }}, // 60° [255,0,255]
|
||||||
|
{{ 0.9404, 0.8727, 0.9687 }, { 0.6753, 0.0000, 0.9590 }}, // 75° [190,0,255]
|
||||||
|
{{ 0.9491, 0.8727, 0.9687 }, { 0.7638, 0.0000, 0.9601 }}, // 90° [126,0,255]
|
||||||
|
{{ 0.9539, 0.8727, 0.9689 }, { 0.8115, 0.0000, 0.9609 }}, // 105° [63,0,255]
|
||||||
|
{{ 0.9997, 0.8998, 0.9997 }, { 0.9997, 0.0000, 0.9997 }}, // 120° [0,0,255] (green)
|
||||||
|
{{ 0.9553, 0.8727, 0.9672 }, { 0.8264, 0.0000, 0.9452 }}, // 135° [0,63,255]
|
||||||
|
{{ 0.9555, 0.8727, 0.9621 }, { 0.8266, 0.0000, 0.8937 }}, // 150° [0,126,255]
|
||||||
|
{{ 0.9555, 0.8727, 0.9524 }, { 0.8273, 0.0000, 0.7964 }}, // 165° [0,190,255]
|
||||||
|
{{ 0.9555, 0.8727, 0.9375 }, { 0.8285, 0.0000, 0.6469 }}, // 180° [0,255,255]
|
||||||
|
{{ 0.9557, 0.8727, 0.9091 }, { 0.8301, 0.0000, 0.3648 }}, // 195° [0,255,190]
|
||||||
|
{{ 0.9606, 0.9037, 0.8727 }, { 0.8782, 0.3091, 0.0000 }}, // 210° [0,255,126]
|
||||||
|
{{ 0.9677, 0.9514, 0.8727 }, { 0.9486, 0.7856, 0.0000 }}, // 225° [0,255,63]
|
||||||
|
{{ 0.9997, 0.9997, 0.8998 }, { 0.9997, 0.9997, 0.0000 }}, // 240° [0,255,0] (blue)
|
||||||
|
{{ 0.9652, 0.9652, 0.8727 }, { 0.9245, 0.9252, 0.0000 }}, // 255° [63,255,0]
|
||||||
|
{{ 0.9501, 0.9631, 0.8727 }, { 0.7746, 0.9029, 0.0000 }}, // 270° [126,255,0]
|
||||||
|
{{ 0.9219, 0.9587, 0.8727 }, { 0.4919, 0.8601, 0.0000 }}, // 285° [190,255,0]
|
||||||
|
{{ 0.8786, 0.9521, 0.8727 }, { 0.0584, 0.7946, 0.0000 }}, // 300° [255,255,0]
|
||||||
|
{{ 0.8727, 0.9531, 0.9152 }, { 0.0000, 0.8022, 0.4250 }}, // 315° [255,190,0]
|
||||||
|
{{ 0.8727, 0.9542, 0.9467 }, { 0.0000, 0.8145, 0.7889 }}, // 330° [255,126,0]
|
||||||
|
{{ 0.8728, 0.9547, 0.9631 }, { 0.0000, 0.8207, 0.9044 }} // 345° [255,63,0]
|
||||||
|
}},
|
||||||
|
// Ring 1, min value RGB component value = 35
|
||||||
|
{{
|
||||||
|
{{ 0.8727, 0.9499, 0.9660 }, { 0.0000, 0.7714, 0.9337 }}, // 0° [255,35,39] (red)
|
||||||
|
{{ 0.8727, 0.9255, 0.9665 }, { 0.0000, 0.5268, 0.9365 }}, // 15° [255,35,90]
|
||||||
|
{{ 0.8727, 0.8793, 0.9662 }, { 0.0000, 0.0664, 0.9345 }}, // 30° [255,35,145]
|
||||||
|
{{ 0.9079, 0.8727, 0.9672 }, { 0.3515, 0.0000, 0.9455 }}, // 45° [255,35,200]
|
||||||
|
{{ 0.9270, 0.8727, 0.9680 }, { 0.5429, 0.0000, 0.9519 }}, // 60° [255,35,255]
|
||||||
|
{{ 0.9386, 0.8727, 0.9680 }, { 0.6593, 0.0000, 0.9534 }}, // 75° [200,35,255]
|
||||||
|
{{ 0.9470, 0.8727, 0.9682 }, { 0.7433, 0.0000, 0.9546 }}, // 90° [145,35,255]
|
||||||
|
{{ 0.9524, 0.8727, 0.9682 }, { 0.7961, 0.0000, 0.9552 }}, // 105° [90,35,255]
|
||||||
|
{{ 0.9547, 0.8727, 0.9682 }, { 0.8212, 0.0000, 0.9555 }}, // 120° [39,35,255] (green)
|
||||||
|
{{ 0.9547, 0.8727, 0.9655 }, { 0.8215, 0.0000, 0.9284 }}, // 135° [35,90,255]
|
||||||
|
{{ 0.9550, 0.8727, 0.9600 }, { 0.8217, 0.0000, 0.8712 }}, // 150° [35,145,255]
|
||||||
|
{{ 0.9550, 0.8727, 0.9506 }, { 0.8225, 0.0000, 0.7788 }}, // 165° [35,200,255]
|
||||||
|
{{ 0.9551, 0.8727, 0.9375 }, { 0.8235, 0.0000, 0.6469 }}, // 180° [35,255,255]
|
||||||
|
{{ 0.9547, 0.8727, 0.9150 }, { 0.8212, 0.0000, 0.4226 }}, // 195° [35,255,200]
|
||||||
|
{{ 0.9560, 0.8828, 0.8727 }, { 0.8322, 0.1003, 0.0000 }}, // 210° [35,255,145]
|
||||||
|
{{ 0.9639, 0.9357, 0.8727 }, { 0.9104, 0.6289, 0.0000 }}, // 225° [35,255,90]
|
||||||
|
{{ 0.9675, 0.9610, 0.8727 }, { 0.9475, 0.8803, 0.0000 }}, // 240° [35,255,35] (blue)
|
||||||
|
{{ 0.9596, 0.9597, 0.8727 }, { 0.8686, 0.8686, 0.0000 }}, // 255° [90,255,35]
|
||||||
|
{{ 0.9430, 0.9570, 0.8727 }, { 0.7023, 0.8435, 0.0000 }}, // 270° [145,255,35]
|
||||||
|
{{ 0.9160, 0.9530, 0.8727 }, { 0.4332, 0.8032, 0.0000 }}, // 285° [200,255,35]
|
||||||
|
{{ 0.8780, 0.9472, 0.8728 }, { 0.0518, 0.7458, 0.0000 }}, // 300° [255,255,35]
|
||||||
|
{{ 0.8727, 0.9477, 0.9099 }, { 0.0000, 0.7497, 0.3717 }}, // 315° [255,200,35]
|
||||||
|
{{ 0.8727, 0.9490, 0.9396 }, { 0.0000, 0.7612, 0.6689 }}, // 330° [255,145,35]
|
||||||
|
{{ 0.8727, 0.9496, 0.9580 }, { 0.0000, 0.7683, 0.8512 }} // 345° [255,90,35]
|
||||||
|
}},
|
||||||
|
// Ring 2, min value RGB component value = 73
|
||||||
|
{{
|
||||||
|
{{ 0.8727, 0.9352, 0.9609 }, { 0.0000, 0.6244, 0.8822 }}, // 0° [255,73,76] (red)
|
||||||
|
{{ 0.8727, 0.9035, 0.9616 }, { 0.0000, 0.3068, 0.8888 }}, // 15° [255,73,119]
|
||||||
|
{{ 0.8847, 0.8727, 0.9629 }, { 0.1189, 0.0000, 0.9004 }}, // 30° [255,73,164]
|
||||||
|
{{ 0.9121, 0.8727, 0.9650 }, { 0.3936, 0.0000, 0.9237 }}, // 45° [255,73,209]
|
||||||
|
{{ 0.9270, 0.8727, 0.9665 }, { 0.5431, 0.0000, 0.9367 }}, // 60° [255,73,255]
|
||||||
|
{{ 0.9370, 0.8727, 0.9665 }, { 0.6428, 0.0000, 0.9377 }}, // 75° [209,73,255]
|
||||||
|
{{ 0.9445, 0.8727, 0.9665 }, { 0.7182, 0.0000, 0.9386 }}, // 90° [164,73,255]
|
||||||
|
{{ 0.9499, 0.8727, 0.9667 }, { 0.7722, 0.0000, 0.9391 }}, // 105° [119,73,255]
|
||||||
|
{{ 0.9534, 0.8727, 0.9667 }, { 0.8066, 0.0000, 0.9396 }}, // 120° [73,73,255] (green)
|
||||||
|
{{ 0.9534, 0.8727, 0.9631 }, { 0.8068, 0.0000, 0.9024 }}, // 135° [73,119,255]
|
||||||
|
{{ 0.9536, 0.8727, 0.9570 }, { 0.8073, 0.0000, 0.8435 }}, // 150° [73,164,255]
|
||||||
|
{{ 0.9536, 0.8727, 0.9487 }, { 0.8081, 0.0000, 0.7602 }}, // 165° [73,209,255]
|
||||||
|
{{ 0.9536, 0.8727, 0.9375 }, { 0.8088, 0.0000, 0.6474 }}, // 180° [73,255,255]
|
||||||
|
{{ 0.9526, 0.8727, 0.9201 }, { 0.7990, 0.0000, 0.4736 }}, // 195° [73,255,209]
|
||||||
|
{{ 0.9509, 0.8727, 0.8866 }, { 0.7804, 0.0000, 0.1383 }}, // 210° [73,255,164]
|
||||||
|
{{ 0.9557, 0.9109, 0.8727 }, { 0.8291, 0.3813, 0.0000 }}, // 225° [73,255,119]
|
||||||
|
{{ 0.9606, 0.9455, 0.8727 }, { 0.8798, 0.7268, 0.0000 }}, // 240° [73,255,73] (blue)
|
||||||
|
{{ 0.9499, 0.9439, 0.8727 }, { 0.7714, 0.7107, 0.0000 }}, // 255° [119,255,73]
|
||||||
|
{{ 0.9329, 0.9414, 0.8727 }, { 0.6005, 0.6855, 0.0000 }}, // 270° [164,255,73]
|
||||||
|
{{ 0.9086, 0.9377, 0.8727 }, { 0.5381, 0.6496, 0.0000 }}, // 285° [209,255,73]
|
||||||
|
{{ 0.8760, 0.9329, 0.8727 }, { 0.0314, 0.6013, 0.0000 }}, // 300° [255,255,73]
|
||||||
|
{{ 0.8727, 0.9331, 0.9052 }, { 0.0000, 0.6027, 0.3248 }}, // 315° [255,209,73]
|
||||||
|
{{ 0.8727, 0.9340, 0.9316 }, { 0.0000, 0.6130, 0.5876 }}, // 330° [255,164,73]
|
||||||
|
{{ 0.8727, 0.9347, 0.9499 }, { 0.0000, 0.6202, 0.7717 }} // 345° [255,119,73]
|
||||||
|
}},
|
||||||
|
// Ring 3, min value RGB component value = 109
|
||||||
|
{{
|
||||||
|
{{ 0.8727, 0.9114, 0.9526 }, { 0.0000, 0.3850, 0.7983 }}, // 0° [255,109,112] (red)
|
||||||
|
{{ 0.8727, 0.8788, 0.9541 }, { 0.0000, 0.0615, 0.8125 }}, // 15° [255,109,145]
|
||||||
|
{{ 0.8979, 0.8727, 0.9587 }, { 0.2526, 0.0000, 0.8586 }}, // 30° [255,109,182]
|
||||||
|
{{ 0.9160, 0.8727, 0.9619 }, { 0.4311, 0.0000, 0.8906 }}, // 45° [255,109,218]
|
||||||
|
{{ 0.9272, 0.8727, 0.9639 }, { 0.6254, 0.0000, 0.9111 }}, // 60° [255,109,255]
|
||||||
|
{{ 0.9352, 0.8727, 0.9639 }, { 0.6254, 0.0000, 0.9111 }}, // 75° [218,109,255]
|
||||||
|
{{ 0.9419, 0.8727, 0.9639 }, { 0.6909, 0.0000, 0.9116 }}, // 90° [182,109,255]
|
||||||
|
{{ 0.9472, 0.8727, 0.9639 }, { 0.7438, 0.0000, 0.9119 }}, // 105° [145,109,255]
|
||||||
|
{{ 0.9509, 0.8727, 0.9640 }, { 0.7814, 0.0000, 0.9121 }}, // 120° [109,109,255] (green)
|
||||||
|
{{ 0.9509, 0.7827, 0.9599 }, { 0.7819, 0.0000, 0.7813 }}, // 135° [109,145,255]
|
||||||
|
{{ 0.9509, 0.7827, 0.9541 }, { 0.7824, 0.0000, 0.8135 }}, // 150° [109,182,255]
|
||||||
|
{{ 0.9511, 0.7827, 0.9467 }, { 0.7829, 0.0000, 0.7409 }}, // 165° [109,218,255]
|
||||||
|
{{ 0.9512, 0.7827, 0.9376 }, { 0.7837, 0.0000, 0.6484 }}, // 180° [109,255,255]
|
||||||
|
{{ 0.9494, 0.7827, 0.9245 }, { 0.7661, 0.0000, 0.5188 }}, // 195° [109,255,218]
|
||||||
|
{{ 0.9465, 0.7827, 0.9035 }, { 0.7373, 0.0000, 0.3073 }}, // 210° [109,255,182]
|
||||||
|
{{ 0.9435, 0.8810, 0.8727 }, { 0.7077, 0.0083, 0.0000 }}, // 225° [109,255,145]
|
||||||
|
{{ 0.9491, 0.9190, 0.8727 }, { 0.7633, 0.4624, 0.0000 }}, // 240° [109,255,109] (blue)
|
||||||
|
{{ 0.9372, 0.9171, 0.8727 }, { 0.6443, 0.4450, 0.0000 }}, // 255° [145,255,109]
|
||||||
|
{{ 0.9204, 0.9147, 0.8727 }, { 0.4760, 0.4209, 0.0000 }}, // 270° [182,255,109]
|
||||||
|
{{ 0.8991, 0.9119, 0.8727 }, { 0.2651, 0.3906, 0.0000 }}, // 285° [218,255,109]
|
||||||
|
{{ 0.8727, 0.9081, 0.8730 }, { 0.0000, 0.3540, 0.0034 }}, // 300° [255,255,109]
|
||||||
|
{{ 0.8727, 0.9094, 0.9016 }, { 0.0000, 0.3652, 0.2871 }}, // 315° [255,218,109]
|
||||||
|
{{ 0.8727, 0.9101, 0.9236 }, { 0.0000, 0.3737, 0.5083 }}, // 330° [255,182,109]
|
||||||
|
{{ 0.8727, 0.9109, 0.9411 }, { 0.0000, 0.3805, 0.6833 }} // 345° [255,145,109]
|
||||||
|
}},
|
||||||
|
// Ring 4, min value RGB component value = 145
|
||||||
|
{{
|
||||||
|
{{ 0.8727, 0.8783, 0.9416 }, { 0.0000, 0.0566, 0.6879 }}, // 0° [255,145,147] (red)
|
||||||
|
{{ 0.8916, 0.8727, 0.9484 }, { 0.1869, 0.0000, 0.7575 }}, // 15° [255,145,172]
|
||||||
|
{{ 0.9081, 0.8727, 0.9539 }, { 0.3550, 0.0000, 0.8110 }}, // 30° [255,145,200]
|
||||||
|
{{ 0.9191, 0.8727, 0.9572 }, { 0.4646, 0.0000, 0.8460 }}, // 45° [255,145,227]
|
||||||
|
{{ 0.9272, 0.8727, 0.9600 }, { 0.5444, 0.0000, 0.8712 }}, // 60° [255,145,255]
|
||||||
|
{{ 0.9336, 0.8727, 0.9600 }, { 0.6074, 0.0000, 0.8713 }}, // 75° [227,145,255]
|
||||||
|
{{ 0.9387, 0.8727, 0.9600 }, { 0.6603, 0.0000, 0.8712 }}, // 90° [200,145,255]
|
||||||
|
{{ 0.9435, 0.8727, 0.9600 }, { 0.7071, 0.0000, 0.8712 }}, // 105° [172,145,255]
|
||||||
|
{{ 0.9472, 0.8727, 0.9600 }, { 0.7442, 0.0000, 0.8713 }}, // 120° [145,145,255] (green)
|
||||||
|
{{ 0.9472, 0.8727, 0.9557 }, { 0.7446, 0.0000, 0.8311 }}, // 135° [145,172,255]
|
||||||
|
{{ 0.9472, 0.8727, 0.9506 }, { 0.7451, 0.0000, 0.7798 }}, // 150° [145,200,255]
|
||||||
|
{{ 0.9472, 0.8727, 0.9447 }, { 0.7456, 0.0000, 0.7208 }}, // 165° [145,227,255]
|
||||||
|
{{ 0.9475, 0.8727, 0.9377 }, { 0.7463, 0.0000, 0.6498 }}, // 180° [145,255,255]
|
||||||
|
{{ 0.9450, 0.8727, 0.9286 }, { 0.7228, 0.0000, 0.5588 }}, // 195° [145,255,227]
|
||||||
|
{{ 0.9419, 0.8727, 0.9160 }, { 0.6904, 0.0000, 0.4319 }}, // 210° [145,255,200]
|
||||||
|
{{ 0.9367, 0.8727, 0.8960 }, { 0.6393, 0.0000, 0.2328 }}, // 225° [145,255,172]
|
||||||
|
{{ 0.9316, 0.8793, 0.8727 }, { 0.5890, 0.0666, 0.0000 }}, // 240° [145,255,145] (blue)
|
||||||
|
{{ 0.9199, 0.8778, 0.8727 }, { 0.4716, 0.0505, 0.0000 }}, // 255° [172,255,145]
|
||||||
|
{{ 0.9050, 0.8757, 0.8727 }, { 0.3227, 0.0003, 0.0000 }}, // 270° [200,255,145]
|
||||||
|
{{ 0.8878, 0.8735, 0.8727 }, { 0.1513, 0.0064, 0.0000 }}, // 285° [227,255,145]
|
||||||
|
{{ 0.8727, 0.8760, 0.8781 }, { 0.0000, 0.0316, 0.0528 }}, // 300° [255,255,145]
|
||||||
|
{{ 0.8727, 0.8766, 0.8990 }, { 0.0000, 0.0398, 0.2607 }}, // 315° [255,227,145]
|
||||||
|
{{ 0.8727, 0.8775, 0.9160 }, { 0.0000, 0.0465, 0.4316 }}, // 330° [255,200,145]
|
||||||
|
{{ 0.8727, 0.8778, 0.9306 }, { 0.0000, 0.0523, 0.5798 }} // 345° [255,172,145]
|
||||||
|
}},
|
||||||
|
// Ring 5, min value RGB component value = 181
|
||||||
|
{{
|
||||||
|
{{ 0.8980, 0.8727, 0.9391 }, { 0.5784, 0.4409, 0.8030 }}, // 0° [255,181,182] (red)
|
||||||
|
{{ 0.9079, 0.8727, 0.9445 }, { 0.6330, 0.4409, 0.8327 }}, // 15° [255,181,199]
|
||||||
|
{{ 0.9162, 0.8727, 0.9486 }, { 0.6774, 0.4409, 0.8556 }}, // 30° [255,181,218]
|
||||||
|
{{ 0.9221, 0.8727, 0.9519 }, { 0.7104, 0.4409, 0.8725 }}, // 45° [255,181,236]
|
||||||
|
{{ 0.9272, 0.8727, 0.9545 }, { 0.7382, 0.4409, 0.8866 }}, // 60° [255,181,255]
|
||||||
|
{{ 0.9316, 0.8727, 0.9545 }, { 0.7618, 0.4409, 0.8863 }}, // 75° [236,181,255]
|
||||||
|
{{ 0.9355, 0.8727, 0.9544 }, { 0.7827, 0.4409, 0.8861 }}, // 90° [218,181,255]
|
||||||
|
{{ 0.9391, 0.8727, 0.9542 }, { 0.8025, 0.4409, 0.8859 }}, // 105° [199,181,255]
|
||||||
|
{{ 0.9421, 0.8727, 0.9542 }, { 0.8191, 0.4409, 0.8859 }}, // 120° [181,181,255] (green)
|
||||||
|
{{ 0.9421, 0.8727, 0.9509 }, { 0.8193, 0.4409, 0.8676 }}, // 135° [181,199,255]
|
||||||
|
{{ 0.9421, 0.8727, 0.9470 }, { 0.8195, 0.4409, 0.8458 }}, // 150° [181,218,255]
|
||||||
|
{{ 0.9424, 0.8727, 0.9429 }, { 0.8197, 0.4409, 0.8230 }}, // 165° [181,236,255]
|
||||||
|
{{ 0.9424, 0.8727, 0.9380 }, { 0.8202, 0.4409, 0.7961 }}, // 180° [181,255,255]
|
||||||
|
{{ 0.9399, 0.8727, 0.9321 }, { 0.8071, 0.4409, 0.7651 }}, // 195° [181,255,236]
|
||||||
|
{{ 0.9370, 0.8727, 0.9252 }, { 0.7912, 0.4409, 0.7274 }}, // 210° [181,255,218]
|
||||||
|
{{ 0.9331, 0.8727, 0.9160 }, { 0.7697, 0.4409, 0.6767 }}, // 225° [181,255,199]
|
||||||
|
{{ 0.9282, 0.8727, 0.9045 }, { 0.7431, 0.4409, 0.6135 }}, // 240° [181,255,181] (blue)
|
||||||
|
{{ 0.9221, 0.8727, 0.9050 }, { 0.7094, 0.4409, 0.6164 }}, // 255° [199,255,181]
|
||||||
|
{{ 0.9147, 0.8727, 0.9057 }, { 0.6700, 0.4409, 0.6200 }}, // 270° [218,255,181]
|
||||||
|
{{ 0.9074, 0.8727, 0.9062 }, { 0.6296, 0.4409, 0.6240 }}, // 285° [236,255,181]
|
||||||
|
{{ 0.8988, 0.8727, 0.9072 }, { 0.5834, 0.4409, 0.6282 }}, // 300° [255,255,181]
|
||||||
|
{{ 0.8986, 0.8727, 0.9166 }, { 0.5820, 0.4409, 0.6807 }}, // 315° [255,236,181]
|
||||||
|
{{ 0.8985, 0.8727, 0.9250 }, { 0.5805, 0.4409, 0.7258 }}, // 330° [255,218,181]
|
||||||
|
{{ 0.8981, 0.8727, 0.9329 }, { 0.5793, 0.4409, 0.7687 }} // 345° [255,199,181]
|
||||||
|
}},
|
||||||
|
// Ring 6, min value RGB component value = 219
|
||||||
|
{{
|
||||||
|
{{ 0.9167, 0.8727, 0.9389 }, { 0.4399, 0.0000, 0.6606 }}, // 0° [255,219,219] (red)
|
||||||
|
{{ 0.9199, 0.8727, 0.9414 }, { 0.4711, 0.0000, 0.6850 }}, // 15° [255,219,228]
|
||||||
|
{{ 0.9226, 0.8727, 0.9432 }, { 0.4992, 0.0000, 0.7068 }}, // 30° [255,219,237]
|
||||||
|
{{ 0.9252, 0.8727, 0.9452 }, { 0.5241, 0.0000, 0.7262 }}, // 45° [255,219,246]
|
||||||
|
{{ 0.9275, 0.8727, 0.9472 }, { 0.5465, 0.0000, 0.7437 }}, // 60° [255,219,255]
|
||||||
|
{{ 0.9294, 0.8727, 0.9470 }, { 0.5675, 0.0000, 0.7432 }}, // 75° [246,219,255]
|
||||||
|
{{ 0.9316, 0.8727, 0.9470 }, { 0.5879, 0.0000, 0.7427 }}, // 90° [237,219,255]
|
||||||
|
{{ 0.9335, 0.8727, 0.9470 }, { 0.6074, 0.0000, 0.7424 }}, // 105° [228,219,255]
|
||||||
|
{{ 0.9352, 0.8727, 0.9470 }, { 0.6259, 0.0000, 0.7419 }}, // 120° [219,219,255] (green)
|
||||||
|
{{ 0.9352, 0.8727, 0.9450 }, { 0.6262, 0.0000, 0.7217 }}, // 135° [219,228,255]
|
||||||
|
{{ 0.9355, 0.8727, 0.9429 }, { 0.6264, 0.0000, 0.7002 }}, // 150° [219,237,255]
|
||||||
|
{{ 0.9355, 0.8727, 0.9406 }, { 0.6269, 0.0000, 0.6777 }}, // 165° [219,246,255]
|
||||||
|
{{ 0.9355, 0.8727, 0.9381 }, { 0.6272, 0.0000, 0.6540 }}, // 180° [219,255,255]
|
||||||
|
{{ 0.9339, 0.8727, 0.9357 }, { 0.6111, 0.0000, 0.6291 }}, // 195° [219,255,246]
|
||||||
|
{{ 0.9321, 0.8727, 0.9329 }, { 0.5929, 0.0000, 0.6013 }}, // 210° [219,255,237]
|
||||||
|
{{ 0.9301, 0.8727, 0.9299 }, { 0.5729, 0.0000, 0.5703 }}, // 225° [219,255,228]
|
||||||
|
{{ 0.9277, 0.8727, 0.9263 }, { 0.5503, 0.0000, 0.5354 }}, // 240° [219,255,219] (blue)
|
||||||
|
{{ 0.9252, 0.8727, 0.9265 }, { 0.5249, 0.0000, 0.5368 }}, // 255° [228,255,219]
|
||||||
|
{{ 0.9226, 0.8727, 0.9265 }, { 0.4985, 0.0000, 0.5383 }}, // 270° [237,255,219]
|
||||||
|
{{ 0.9199, 0.8727, 0.9267 }, { 0.4711, 0.0000, 0.5399 }}, // 285° [246,255,219]
|
||||||
|
{{ 0.9170, 0.8727, 0.9270 }, { 0.4426, 0.0000, 0.5414 }}, // 300° [255,255,219]
|
||||||
|
{{ 0.9170, 0.8727, 0.9301 }, { 0.4419, 0.0000, 0.5734 }}, // 315° [255,246,219]
|
||||||
|
{{ 0.9170, 0.8727, 0.9331 }, { 0.4411, 0.0000, 0.6039 }}, // 330° [255,237,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,
|
||||||
|
* based on RGB color values + brightness.
|
||||||
|
*/
|
||||||
|
class ColorHandlerRGB : public ColorHandler {
|
||||||
|
public:
|
||||||
|
bool set_light_color_values(light::LightColorValues v) {
|
||||||
|
light_mode = LIGHT_MODE_RGB;
|
||||||
|
|
||||||
|
if (v.get_color_mode() != light::ColorMode::RGB) {
|
||||||
|
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 = std::min(std::min(v.get_red(), v.get_green()), v.get_blue());
|
||||||
|
auto level = 7.0f * rgb_min;
|
||||||
|
|
||||||
|
// 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 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_);
|
||||||
|
|
||||||
|
// 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_);
|
||||||
|
|
||||||
|
// 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 = std::lerp(rgb_a_.red, rgb_b_.red, d);
|
||||||
|
green = std::lerp(rgb_a_.green, rgb_b_.green, d);
|
||||||
|
blue = std::lerp(rgb_a_.blue, rgb_b_.blue, d);
|
||||||
|
|
||||||
|
// The white output channel will always be 0 for RGB.
|
||||||
|
white = 0.0f;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
RGBPoint rgbp_a_;
|
||||||
|
RGBPoint rgbp_b_;
|
||||||
|
RGB rgb_a_;
|
||||||
|
RGB rgb_b_;
|
||||||
|
|
||||||
|
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];
|
||||||
|
|
||||||
|
// 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];
|
||||||
|
|
||||||
|
// Interpolate based on the ring position.
|
||||||
|
auto d = pos - pos_x;
|
||||||
|
p->low.red = std::lerp(x.low.red, y.low.red, d);
|
||||||
|
p->low.green = std::lerp(x.low.green, y.low.green, d);
|
||||||
|
p->low.blue = std::lerp(x.low.blue, y.low.blue, d);
|
||||||
|
p->high.red = std::lerp(x.high.red, y.high.red, d);
|
||||||
|
p->high.green = std::lerp(x.high.green, y.high.green, d);
|
||||||
|
p->high.blue = std::lerp(x.high.blue, y.high.blue, d);
|
||||||
|
|
||||||
|
// 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 = std::min(std::min(red, green), blue);
|
||||||
|
auto rgb_max = std::max(std::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 = std::lerp(p->low.red, p->high.red, d);
|
||||||
|
rgb->green = std::lerp(p->low.green, p->high.green, d);
|
||||||
|
rgb->blue = std::lerp(p->low.blue, p->high.blue, d);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace bslamp2
|
||||||
|
} // namespace xiaomi
|
||||||
|
} // namespace esphome
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
namespace bslamp2 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an interface definition that is used to extend the LightState
|
||||||
|
* class with functionality for disco actions (immediate light updates,
|
||||||
|
* not publishing or saving the light state).
|
||||||
|
*
|
||||||
|
* This interface is required by the DiscoAction class.
|
||||||
|
*/
|
||||||
|
class LightStateDiscoSupport {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Stop the disco, by restoring the previously remembered light state.
|
||||||
|
*/
|
||||||
|
virtual void disco_stop() = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do not wait until the next loop() call for the light to write the
|
||||||
|
* requested state to the light output, but write the new state
|
||||||
|
* right away.
|
||||||
|
*
|
||||||
|
* This allows us to update the state of the light, even when we are
|
||||||
|
* being called in the middle of another component's loop().
|
||||||
|
*/
|
||||||
|
virtual void disco_apply() = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a light::LightCall object, with some properties already
|
||||||
|
* configured for using it as a disco call.
|
||||||
|
*/
|
||||||
|
virtual light::LightCall make_disco_call(bool save_and_publish) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace bslamp2
|
||||||
|
} // namespace xiaomi
|
||||||
|
} // namespace esphome
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../common.h"
|
||||||
|
#include "../light_hal.h"
|
||||||
|
#include "color_handler_chain.h"
|
||||||
|
#include "light_transformer.h"
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/components/ledc/ledc_output.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
namespace bslamp2 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A LightOutput class for the Xiaomi Mijia Bedside Lamp 2.
|
||||||
|
*
|
||||||
|
* The function of this class is to translate a required light state
|
||||||
|
* into actual physicial GPIO output signals to drive the device's LED
|
||||||
|
* circuitry. It forms the glue between the physical device and the
|
||||||
|
* logical light color input.
|
||||||
|
*/
|
||||||
|
class XiaomiBslamp2LightOutput : public Component, public light::LightOutput {
|
||||||
|
public:
|
||||||
|
void set_parent(LightHAL *light) { light_ = light; }
|
||||||
|
|
||||||
|
void set_night_light_color_temperature_calibration(float red, float green, float blue) {
|
||||||
|
night_light_calibration_ = {red, green, blue};
|
||||||
|
color_handler_chain.set_night_light_color_temperature_calibration(night_light_calibration_);
|
||||||
|
apply_current_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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_supported_color_modes({light::ColorMode::RGB, light::ColorMode::COLOR_TEMPERATURE});
|
||||||
|
traits.set_min_mireds(MIRED_MIN);
|
||||||
|
traits.set_max_mireds(MIRED_MAX);
|
||||||
|
return traits;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<light::LightTransformer> create_default_transition() override {
|
||||||
|
return make_unique<XiaomiBslamp2LightTransitionTransformer>(
|
||||||
|
light_,
|
||||||
|
&light_mode_callback_,
|
||||||
|
&state_callback_,
|
||||||
|
night_light_calibration_);
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a requested light state to the physicial GPIO outputs.
|
||||||
|
*/
|
||||||
|
void write_state(light::LightState *state) {
|
||||||
|
state_ = state;
|
||||||
|
auto values = state->current_values;
|
||||||
|
|
||||||
|
color_handler_chain.set_light_color_values(values);
|
||||||
|
light_mode_callback_.call(color_handler_chain.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();
|
||||||
|
|
||||||
|
// Apply the GPIO output levels as defined by the color handler.
|
||||||
|
light_->set_state(&color_handler_chain);
|
||||||
|
|
||||||
|
if (values.get_state() == 0)
|
||||||
|
light_->turn_off();
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_current_state() {
|
||||||
|
if (state_ != nullptr)
|
||||||
|
write_state(state_);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
LightHAL *light_;
|
||||||
|
light::LightState *state_{nullptr};
|
||||||
|
ColorHandlerChain color_handler_chain;
|
||||||
|
CallbackManager<void(std::string)> light_mode_callback_{};
|
||||||
|
CallbackManager<void(light::LightColorValues)> state_callback_{};
|
||||||
|
NightLightCalibration night_light_calibration_{DEFAULT_NIGHT_LIGHT_CALIBRATION};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace bslamp2
|
||||||
|
} // namespace xiaomi
|
||||||
|
} // namespace esphome
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../common.h"
|
||||||
|
#include "interfaces.h"
|
||||||
|
#include "light_output.h"
|
||||||
|
#include "esphome/components/light/light_state.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
namespace bslamp2 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom LightState class for the Xiaomi Bedside Lamp 2.
|
||||||
|
*
|
||||||
|
* It is used by the DiscoAction to apply immediate light output
|
||||||
|
* updates, without saving or publishing the new state.
|
||||||
|
*/
|
||||||
|
class XiaomiBslamp2LightState : public light::LightState, public LightStateDiscoSupport {
|
||||||
|
public:
|
||||||
|
XiaomiBslamp2LightState(XiaomiBslamp2LightOutput *output) : light::LightState(output) { }
|
||||||
|
|
||||||
|
void disco_stop() {
|
||||||
|
light::LightStateRTCState recovered{};
|
||||||
|
if (this->rtc_.load(&recovered)) {
|
||||||
|
auto call = make_disco_call(true);
|
||||||
|
call.set_state(recovered.state);
|
||||||
|
call.set_brightness_if_supported(recovered.brightness);
|
||||||
|
call.set_color_brightness_if_supported(recovered.color_brightness);
|
||||||
|
call.set_red_if_supported(recovered.red);
|
||||||
|
call.set_green_if_supported(recovered.green);
|
||||||
|
call.set_blue_if_supported(recovered.blue);
|
||||||
|
call.set_white_if_supported(recovered.white);
|
||||||
|
call.set_color_temperature_if_supported(recovered.color_temp);
|
||||||
|
call.set_cold_white_if_supported(recovered.cold_white);
|
||||||
|
call.set_warm_white_if_supported(recovered.warm_white);
|
||||||
|
if (recovered.effect != 0) {
|
||||||
|
call.set_effect(recovered.effect);
|
||||||
|
} else {
|
||||||
|
call.set_transition_length_if_supported(0);
|
||||||
|
}
|
||||||
|
call.set_color_mode_if_supported(recovered.color_mode);
|
||||||
|
call.perform();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void disco_apply() {
|
||||||
|
this->output_->write_state(this);
|
||||||
|
this->next_write_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_night_light_color_temperature_calibration(float red, float green, float blue) {
|
||||||
|
auto *output = static_cast<XiaomiBslamp2LightOutput *>(this->output_);
|
||||||
|
output->set_night_light_color_temperature_calibration(red, green, blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
light::LightCall make_disco_call(bool save_and_publish) {
|
||||||
|
auto call = this->make_call();
|
||||||
|
call.set_save(save_and_publish);
|
||||||
|
call.set_publish(save_and_publish);
|
||||||
|
return call;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace bslamp2
|
||||||
|
} // namespace xiaomi
|
||||||
|
} // namespace esphome
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../common.h"
|
||||||
|
#include "../light_hal.h"
|
||||||
|
#include "color_handler_chain.h"
|
||||||
|
#include "esphome/components/light/light_transformer.h"
|
||||||
|
#include "esphome/components/light/light_color_values.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
namespace bslamp2 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A LightTransitionTransformer class for the Xiaomi Mijia Bedside Lamp 2.
|
||||||
|
*/
|
||||||
|
class XiaomiBslamp2LightTransitionTransformer : public light::LightTransitionTransformer {
|
||||||
|
public:
|
||||||
|
explicit XiaomiBslamp2LightTransitionTransformer(
|
||||||
|
LightHAL *light,
|
||||||
|
CallbackManager<void(std::string)> *light_mode_callback,
|
||||||
|
CallbackManager<void(light::LightColorValues)> *state_callback,
|
||||||
|
NightLightCalibration night_light_calibration) :
|
||||||
|
light_(light),
|
||||||
|
light_mode_callback_(light_mode_callback),
|
||||||
|
state_callback_(state_callback),
|
||||||
|
night_light_calibration_(night_light_calibration) { }
|
||||||
|
|
||||||
|
bool is_finished() override {
|
||||||
|
return force_finish_ || get_progress_() >= 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void start() override {
|
||||||
|
// Determine the GPIO outputs to use for the start and end point.
|
||||||
|
// This light transition transformer will then transition linearly between them.
|
||||||
|
light_->copy_to(&start_);
|
||||||
|
end_.set_night_light_color_temperature_calibration(night_light_calibration_);
|
||||||
|
end_.set_light_color_values(target_values_);
|
||||||
|
|
||||||
|
// Update the light mode of the light HAL to the target state, unless
|
||||||
|
// this is night mode. For night mode, the update is done after the
|
||||||
|
// state has been reached. This makes sure that forcing instant
|
||||||
|
// transitions for night light to night light is only done when the
|
||||||
|
// night light status has actually been reached. E.g. when in RGB mode
|
||||||
|
// and transitioning to night light in 10 seconds, interrupting this
|
||||||
|
// after 5 seconds with a new night light setting should not make the
|
||||||
|
// transition instant.
|
||||||
|
if (end_.light_mode != LIGHT_MODE_NIGHT) {
|
||||||
|
light_->set_light_mode(end_.light_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run callbacks. These are normally called from the LightOutput, but
|
||||||
|
// since I don't call LightOutput::write_state() from this transformer's
|
||||||
|
// code, these callbacks must be called from this transformer instead.
|
||||||
|
light_mode_callback_->call(end_.light_mode);
|
||||||
|
state_callback_->call(target_values_);
|
||||||
|
}
|
||||||
|
|
||||||
|
optional<light::LightColorValues> apply() override {
|
||||||
|
// When transitioning between off and night mode, or between night mode
|
||||||
|
// light colors, then do this immediately.
|
||||||
|
// The LED driver circuitry is not capable of doing clean color or brightness
|
||||||
|
// transitions at the low levels as used for the night light.
|
||||||
|
if ((end_.light_mode == LIGHT_MODE_NIGHT &&
|
||||||
|
(start_.light_mode == LIGHT_MODE_OFF || start_.light_mode == LIGHT_MODE_NIGHT)) ||
|
||||||
|
(end_.light_mode == LIGHT_MODE_OFF && start_.light_mode == LIGHT_MODE_NIGHT)) {
|
||||||
|
light_->set_state(&end_);
|
||||||
|
if (end_.light_mode != LIGHT_MODE_OFF) {
|
||||||
|
light_->turn_on();
|
||||||
|
}
|
||||||
|
force_finish_ = true;
|
||||||
|
}
|
||||||
|
// Otherwise perform a standard transformation.
|
||||||
|
else {
|
||||||
|
auto smoothed = light::LightTransitionTransformer::smoothed_progress(get_progress_());
|
||||||
|
light_->set_rgbw(
|
||||||
|
std::lerp(start_.red, end_.red, smoothed),
|
||||||
|
std::lerp(start_.green, end_.green, smoothed),
|
||||||
|
std::lerp(start_.blue, end_.blue, smoothed),
|
||||||
|
std::lerp(start_.white, end_.white, smoothed));
|
||||||
|
if (end_.light_mode != LIGHT_MODE_OFF) {
|
||||||
|
light_->turn_on();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_finished()) {
|
||||||
|
light_->set_light_mode(end_.light_mode);
|
||||||
|
if (end_.light_mode == LIGHT_MODE_OFF) {
|
||||||
|
light_->turn_off();
|
||||||
|
}
|
||||||
|
|
||||||
|
return target_values_;
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
LightHAL *light_;
|
||||||
|
bool force_finish_{false};
|
||||||
|
GPIOOutputValues start_{};
|
||||||
|
ColorHandlerChain end_{};
|
||||||
|
CallbackManager<void(std::string)> *light_mode_callback_;
|
||||||
|
CallbackManager<void(light::LightColorValues)> *state_callback_;
|
||||||
|
NightLightCalibration night_light_calibration_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace bslamp2
|
||||||
|
} // namespace xiaomi
|
||||||
|
} // namespace esphome
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../common.h"
|
||||||
|
#include "esphome/core/optional.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
namespace bslamp2 {
|
||||||
|
|
||||||
|
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_;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class PresetsContainer : public Component {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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_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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/gpio/output/gpio_binary_output.h"
|
||||||
|
#include "esphome/components/ledc/ledc_output.h"
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
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"};
|
||||||
|
|
||||||
|
class GPIOOutputValues {
|
||||||
|
public:
|
||||||
|
float red = 0.0f;
|
||||||
|
float green = 0.0f;
|
||||||
|
float blue = 0.0f;
|
||||||
|
float white = 0.0f;
|
||||||
|
std::string light_mode = LIGHT_MODE_OFF;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the current output values to another GPIOOutputValues object.
|
||||||
|
*/
|
||||||
|
void copy_to(GPIOOutputValues *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); }
|
||||||
|
};
|
||||||
|
|
||||||
|
class LightHAL : public Component, public GPIOOutputValues {
|
||||||
|
public:
|
||||||
|
void set_red_pin(ledc::LEDCOutput *pin) { red_pin_ = pin; }
|
||||||
|
void set_green_pin(ledc::LEDCOutput *pin) { green_pin_ = pin; }
|
||||||
|
void set_blue_pin(ledc::LEDCOutput *pin) { blue_pin_ = pin; }
|
||||||
|
void set_white_pin(ledc::LEDCOutput *pin) { white_pin_ = pin; }
|
||||||
|
void set_master1_pin(gpio::GPIOBinaryOutput *pin) { master1_pin_ = pin; }
|
||||||
|
void set_master2_pin(gpio::GPIOBinaryOutput *pin) { master2_pin_ = pin; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn on the master switch for the LEDs.
|
||||||
|
*/
|
||||||
|
void turn_on() {
|
||||||
|
master1_pin_->turn_on();
|
||||||
|
master2_pin_->turn_on();
|
||||||
|
is_on_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn off the master switch for the LEDs.
|
||||||
|
*/
|
||||||
|
void turn_off() {
|
||||||
|
master1_pin_->turn_off();
|
||||||
|
master2_pin_->turn_off();
|
||||||
|
is_on_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the light is turned on.
|
||||||
|
*/
|
||||||
|
bool is_on() {
|
||||||
|
return is_on_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_state(GPIOOutputValues *new_state) {
|
||||||
|
new_state->copy_to(this);
|
||||||
|
red_pin_->set_level(this->red);
|
||||||
|
green_pin_->set_level(this->green);
|
||||||
|
blue_pin_->set_level(this->blue);
|
||||||
|
white_pin_->set_level(this->white);
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_rgbw(float r, float g, float b, float w) {
|
||||||
|
red_pin_->set_level(r);
|
||||||
|
green_pin_->set_level(g);
|
||||||
|
blue_pin_->set_level(b);
|
||||||
|
white_pin_->set_level(w);
|
||||||
|
|
||||||
|
this->red = r;
|
||||||
|
this->green = g;
|
||||||
|
this->blue = b;
|
||||||
|
this->white = w;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_light_mode(std::string light_mode) {
|
||||||
|
this->light_mode = light_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool is_on_{false};
|
||||||
|
ledc::LEDCOutput *red_pin_;
|
||||||
|
ledc::LEDCOutput *green_pin_;
|
||||||
|
ledc::LEDCOutput *blue_pin_;
|
||||||
|
ledc::LEDCOutput *white_pin_;
|
||||||
|
gpio::GPIOBinaryOutput *master1_pin_;
|
||||||
|
gpio::GPIOBinaryOutput *master2_pin_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace bslamp2
|
||||||
|
} // namespace xiaomi
|
||||||
|
} // namespace esphome
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.components import output
|
||||||
|
from esphome.const import CONF_ID, CONF_LEVEL
|
||||||
|
from esphome import automation
|
||||||
|
from .. import bslamp2_ns, CODEOWNERS, CONF_FRONT_PANEL_HAL_ID, FrontPanelHAL, FRONT_PANEL_LED_OPTIONS
|
||||||
|
|
||||||
|
__all__ = ["CODEOWNERS"]
|
||||||
|
|
||||||
|
CONF_LEDS = "leds"
|
||||||
|
|
||||||
|
DEPENDENCIES = ["xiaomi_bslamp2"]
|
||||||
|
|
||||||
|
XiaomiBslamp2FrontPanelOutput = bslamp2_ns.class_("XiaomiBslamp2FrontPanelOutput", output.FloatOutput, cg.Component)
|
||||||
|
SetLEDsAction = bslamp2_ns.class_("SetLEDsAction", automation.Action)
|
||||||
|
SetLevelAction = bslamp2_ns.class_("SetLevelAction", automation.Action)
|
||||||
|
UpdateLEDsAction = bslamp2_ns.class_("UpdateLEDsAction", automation.Action)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(XiaomiBslamp2FrontPanelOutput),
|
||||||
|
cv.GenerateID(CONF_FRONT_PANEL_HAL_ID): cv.use_id(FrontPanelHAL),
|
||||||
|
}
|
||||||
|
).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
|
def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
yield cg.register_component(var, config)
|
||||||
|
yield output.register_output(var, config)
|
||||||
|
|
||||||
|
front_panel_hal_var = yield cg.get_variable(config[CONF_FRONT_PANEL_HAL_ID])
|
||||||
|
cg.add(var.set_parent(front_panel_hal_var))
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_simple_level_value(schema):
|
||||||
|
def validator(value):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return schema(value)
|
||||||
|
return schema({"level": value})
|
||||||
|
|
||||||
|
return validator
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_simple_leds_value(schema):
|
||||||
|
def validator(value):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return schema(value)
|
||||||
|
return schema({"leds": value})
|
||||||
|
|
||||||
|
return validator
|
||||||
|
|
||||||
|
|
||||||
|
FRONT_PANEL_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_ID): cv.use_id(XiaomiBslamp2FrontPanelOutput),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
FRONT_PANEL_LEVEL_SCHEMA = cv.Schema(
|
||||||
|
maybe_simple_level_value(
|
||||||
|
FRONT_PANEL_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_LEVEL): cv.templatable(cv.percentage),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
FRONT_PANEL_LED_SCHEMA = cv.Schema(
|
||||||
|
maybe_simple_leds_value(
|
||||||
|
FRONT_PANEL_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_LEDS): cv.ensure_list(cv.enum(FRONT_PANEL_LED_OPTIONS, upper=True)),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"front_panel.set_level",
|
||||||
|
SetLevelAction,
|
||||||
|
FRONT_PANEL_LEVEL_SCHEMA,
|
||||||
|
synchronous=True,
|
||||||
|
)
|
||||||
|
async def set_level_to_code(config, action_id, template_arg, args):
|
||||||
|
output_var = await cg.get_variable(config[CONF_ID])
|
||||||
|
action_var = cg.new_Pvariable(action_id, template_arg, output_var)
|
||||||
|
template_ = await cg.templatable(config[CONF_LEVEL], args, float)
|
||||||
|
cg.add(action_var.set_level(template_))
|
||||||
|
return action_var
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"front_panel.set_leds",
|
||||||
|
SetLEDsAction,
|
||||||
|
FRONT_PANEL_LED_SCHEMA,
|
||||||
|
synchronous=True,
|
||||||
|
)
|
||||||
|
async def set_leds_to_code(config, action_id, template_arg, args):
|
||||||
|
output_var = await cg.get_variable(config[CONF_ID])
|
||||||
|
action_var = cg.new_Pvariable(action_id, template_arg, output_var)
|
||||||
|
bits = [FRONT_PANEL_LED_OPTIONS["NONE"]] + [FRONT_PANEL_LED_OPTIONS[led] for led in config[CONF_LEDS]]
|
||||||
|
value = cg.RawExpression("|".join(map(str, bits)))
|
||||||
|
mode_template = await cg.templatable(2, args, cg.int_)
|
||||||
|
cg.add(action_var.set_mode(mode_template))
|
||||||
|
leds_template = await cg.templatable(value, args, cg.uint16)
|
||||||
|
cg.add(action_var.set_leds(leds_template))
|
||||||
|
return action_var
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"front_panel.turn_on_leds",
|
||||||
|
SetLEDsAction,
|
||||||
|
FRONT_PANEL_LED_SCHEMA,
|
||||||
|
synchronous=True,
|
||||||
|
)
|
||||||
|
async def turn_on_leds_to_code(config, action_id, template_arg, args):
|
||||||
|
output_var = await cg.get_variable(config[CONF_ID])
|
||||||
|
action_var = cg.new_Pvariable(action_id, template_arg, output_var)
|
||||||
|
bits = [FRONT_PANEL_LED_OPTIONS["NONE"]] + [FRONT_PANEL_LED_OPTIONS[led] for led in config[CONF_LEDS]]
|
||||||
|
value = cg.RawExpression("|".join(map(str, bits)))
|
||||||
|
mode_template = await cg.templatable(1, args, cg.int_)
|
||||||
|
cg.add(action_var.set_mode(mode_template))
|
||||||
|
leds_template = await cg.templatable(value, args, cg.uint16)
|
||||||
|
cg.add(action_var.set_leds(leds_template))
|
||||||
|
return action_var
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"front_panel.turn_off_leds",
|
||||||
|
SetLEDsAction,
|
||||||
|
FRONT_PANEL_LED_SCHEMA,
|
||||||
|
synchronous=True,
|
||||||
|
)
|
||||||
|
async def turn_off_leds_to_code(config, action_id, template_arg, args):
|
||||||
|
output_var = await cg.get_variable(config[CONF_ID])
|
||||||
|
action_var = cg.new_Pvariable(action_id, template_arg, output_var)
|
||||||
|
bits = [FRONT_PANEL_LED_OPTIONS["NONE"]] + [FRONT_PANEL_LED_OPTIONS[led] for led in config[CONF_LEDS]]
|
||||||
|
value = cg.RawExpression("|".join(map(str, bits)))
|
||||||
|
mode_template = await cg.templatable(0, args, cg.int_)
|
||||||
|
cg.add(action_var.set_mode(mode_template))
|
||||||
|
leds_template = await cg.templatable(value, args, cg.uint16)
|
||||||
|
cg.add(action_var.set_leds(leds_template))
|
||||||
|
return action_var
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"front_panel.update_leds",
|
||||||
|
UpdateLEDsAction,
|
||||||
|
FRONT_PANEL_SCHEMA,
|
||||||
|
synchronous=True,
|
||||||
|
)
|
||||||
|
async def update_leds_to_code(config, action_id, template_arg, args):
|
||||||
|
output_var = await cg.get_variable(config[CONF_ID])
|
||||||
|
action_var = cg.new_Pvariable(action_id, template_arg, output_var)
|
||||||
|
return action_var
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "../front_panel_hal.h"
|
||||||
|
#include "output.h"
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
namespace bslamp2 {
|
||||||
|
|
||||||
|
template<typename... Ts> class SetLEDsAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
explicit SetLEDsAction(XiaomiBslamp2FrontPanelOutput *parent) : parent_(parent) {}
|
||||||
|
|
||||||
|
TEMPLATABLE_VALUE(int, mode)
|
||||||
|
TEMPLATABLE_VALUE(uint16_t, leds)
|
||||||
|
|
||||||
|
void play(const Ts &...x) override {
|
||||||
|
uint16_t mode = this->mode_.value(x...);
|
||||||
|
uint16_t value = this->leds_.value(x...);
|
||||||
|
switch (mode) {
|
||||||
|
case 0:
|
||||||
|
parent_->turn_off_leds(value);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
parent_->turn_on_leds(value);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
parent_->set_leds(value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
XiaomiBslamp2FrontPanelOutput *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class SetLevelAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
explicit SetLevelAction(XiaomiBslamp2FrontPanelOutput *parent) : parent_(parent) {}
|
||||||
|
|
||||||
|
TEMPLATABLE_VALUE(float, level)
|
||||||
|
|
||||||
|
void play(const Ts &...x) override {
|
||||||
|
parent_->set_level(this->level_.value(x...));
|
||||||
|
parent_->update_leds();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
XiaomiBslamp2FrontPanelOutput *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class UpdateLEDsAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
explicit UpdateLEDsAction(XiaomiBslamp2FrontPanelOutput *parent) : parent_(parent) {}
|
||||||
|
|
||||||
|
void play(const Ts &...x) override {
|
||||||
|
parent_->update_leds();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
XiaomiBslamp2FrontPanelOutput *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace bslamp2
|
||||||
|
} // namespace xiaomi
|
||||||
|
} // namespace esphome
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../common.h"
|
||||||
|
#include "../front_panel_hal.h"
|
||||||
|
#include "esphome/components/output/float_output.h"
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
namespace bslamp2 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An output, used for controlling the front panel illumination and
|
||||||
|
* level indicator on the Xiaomi Mijia Bedside Lamp 2 front panel.
|
||||||
|
*/
|
||||||
|
class XiaomiBslamp2FrontPanelOutput : public output::FloatOutput, public Component {
|
||||||
|
public:
|
||||||
|
void set_parent(FrontPanelHAL *front_panel) {
|
||||||
|
front_panel_ = front_panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_state(float level) {
|
||||||
|
if (level > 0) {
|
||||||
|
turn_on_leds(LED_POWER | LED_COLOR);
|
||||||
|
set_level(level);
|
||||||
|
} else {
|
||||||
|
turn_off_leds(LED_ALL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_level(float level) {
|
||||||
|
front_panel_->set_slider_level(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_leds(uint16_t leds) {
|
||||||
|
front_panel_->set_leds(leds);
|
||||||
|
}
|
||||||
|
|
||||||
|
void turn_on_leds(uint16_t leds) {
|
||||||
|
front_panel_->turn_on_leds(leds);
|
||||||
|
}
|
||||||
|
|
||||||
|
void turn_off_leds(uint16_t leds) {
|
||||||
|
front_panel_->turn_off_leds(leds);
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_leds() {
|
||||||
|
front_panel_->update_leds();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
FrontPanelHAL *front_panel_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace bslamp2
|
||||||
|
} // namespace xiaomi
|
||||||
|
} // namespace esphome
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.components import sensor
|
||||||
|
from esphome.const import CONF_ID, CONF_FORCE_UPDATE, CONF_RANGE_FROM, CONF_RANGE_TO
|
||||||
|
from .. import bslamp2_ns, CODEOWNERS, CONF_FRONT_PANEL_HAL_ID, FrontPanelHAL
|
||||||
|
|
||||||
|
__all__ = ["CODEOWNERS"]
|
||||||
|
|
||||||
|
DEPENDENCIES = ["xiaomi_bslamp2"]
|
||||||
|
|
||||||
|
XiaomiBslamp2SliderSensor = bslamp2_ns.class_("XiaomiBslamp2SliderSensor", sensor.Sensor, cg.Component)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
sensor.sensor_schema()
|
||||||
|
.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(XiaomiBslamp2SliderSensor),
|
||||||
|
cv.GenerateID(CONF_FRONT_PANEL_HAL_ID): cv.use_id(FrontPanelHAL),
|
||||||
|
cv.Optional(CONF_FORCE_UPDATE, default=True): cv.boolean,
|
||||||
|
cv.Optional(CONF_RANGE_FROM, default=0.01): cv.float_,
|
||||||
|
cv.Optional(CONF_RANGE_TO, default=1.00): cv.float_,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.extend(cv.COMPONENT_SCHEMA)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
yield cg.register_component(var, config)
|
||||||
|
yield sensor.register_sensor(var, config)
|
||||||
|
|
||||||
|
front_panel_hal_var = yield cg.get_variable(config[CONF_FRONT_PANEL_HAL_ID])
|
||||||
|
cg.add(var.set_parent(front_panel_hal_var))
|
||||||
|
cg.add(var.set_range_from(config[CONF_RANGE_FROM]))
|
||||||
|
cg.add(var.set_range_to(config[CONF_RANGE_TO]))
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../common.h"
|
||||||
|
#include "../front_panel_hal.h"
|
||||||
|
#include "esphome/components/sensor/sensor.h"
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
namespace bslamp2 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A sensor for the touch slider on the front panel of the
|
||||||
|
* Xiaomi Mijia Bedside Lamp 2.
|
||||||
|
*
|
||||||
|
* This sensor publishes the level at which the slider was touched, so it
|
||||||
|
* can be used to implement automations. Note that it does not represent
|
||||||
|
* the brightness of the LED lights (this is implemented by the light output
|
||||||
|
* component), nor the level as displayed by the slider using the front
|
||||||
|
* panel illumination (this is implemented by the slider output 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 = std::max(0.0f, level - 2.0f);
|
||||||
|
|
||||||
|
float final_level = range_from_ + (slope_ * corrected_level);
|
||||||
|
|
||||||
|
// Accomodate for rounding errors that might push the result
|
||||||
|
// value just past the "range to" value.
|
||||||
|
if (final_level > range_to_) {
|
||||||
|
final_level = range_to_;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.components import text_sensor
|
||||||
|
from esphome.const import CONF_ID, CONF_OUTPUT_ID
|
||||||
|
from .. import bslamp2_ns, CODEOWNERS
|
||||||
|
from ..light import XiaomiBslamp2LightOutput
|
||||||
|
|
||||||
|
__all__ = ["CODEOWNERS"]
|
||||||
|
|
||||||
|
DEPENDENCIES = ["xiaomi_bslamp2"]
|
||||||
|
|
||||||
|
CONF_LIGHT_ID = "light_id"
|
||||||
|
|
||||||
|
XiaomiBslamp2LightModeTextSensor = bslamp2_ns.class_(
|
||||||
|
"XiaomiBslamp2LightModeTextSensor", text_sensor.TextSensor, cg.Component
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
text_sensor.text_sensor_schema()
|
||||||
|
.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(XiaomiBslamp2LightModeTextSensor),
|
||||||
|
cv.GenerateID(CONF_OUTPUT_ID): cv.use_id(XiaomiBslamp2LightOutput),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.extend(cv.COMPONENT_SCHEMA)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
yield cg.register_component(var, config)
|
||||||
|
yield text_sensor.register_text_sensor(var, config)
|
||||||
|
|
||||||
|
parent_var = yield cg.get_variable(config[CONF_OUTPUT_ID])
|
||||||
|
cg.add(var.set_parent(parent_var))
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/text_sensor/text_sensor.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace xiaomi {
|
||||||
|
namespace bslamp2 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A text sensor, used for propagating the active light mode on the
|
||||||
|
* Xiaomi Mijia Bedside Lamp 2.
|
||||||
|
*
|
||||||
|
* The possible light modes are "off", "rgb", "white" and "night".
|
||||||
|
*/
|
||||||
|
class XiaomiBslamp2LightModeTextSensor : public text_sensor::TextSensor, public Component {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
XiaomiBslamp2LightOutput *light_;
|
||||||
|
std::string last_light_mode_ = LIGHT_MODE_UNKNOWN;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace bslamp2
|
||||||
|
} // namespace xiaomi
|
||||||
|
} // namespace esphome
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# Behavior: default
|
||||||
|
#
|
||||||
|
# * Presets that resemble the original Xiaomi firmware
|
||||||
|
# (one group of RGB colors, one group of white light colors)
|
||||||
|
# * Tapping the power button, toggles the light
|
||||||
|
# * Holding the power button, turns on night light mode
|
||||||
|
# * Tapping the color button, activates the next preset
|
||||||
|
# * Holding the color button, activates the next preset group
|
||||||
|
# * Touching the slider sets the brightness level and turns on the lamp
|
||||||
|
# * When the light is on, the front panel illumination is on
|
||||||
|
# * When the night light is on, only the power button is illuminated
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# A global variable to keep track of the brightness level. This one is
|
||||||
|
# used by the update_front_panel script to represent the brightness level
|
||||||
|
# using the front panel illumination.
|
||||||
|
#
|
||||||
|
# This variable is used instead of querying the light component directly,
|
||||||
|
# because that brightness level may vary during a light transition. The
|
||||||
|
# behavior that I want: when you touch the front panel slider at 75%, then
|
||||||
|
# the illumination rises directly to that level, while the light might do
|
||||||
|
# a smooth transition.
|
||||||
|
globals:
|
||||||
|
- id: current_brightness
|
||||||
|
type: float
|
||||||
|
initial_value: "0.0"
|
||||||
|
|
||||||
|
# This script updates the front panel illumination.
|
||||||
|
script:
|
||||||
|
- id: update_front_panel
|
||||||
|
mode: restart
|
||||||
|
then:
|
||||||
|
- front_panel.turn_off_leds: ALL
|
||||||
|
# In rgb or white light mode, turn on the front panel illumination,
|
||||||
|
# and show the current brightness level.
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
or:
|
||||||
|
- text_sensor.state:
|
||||||
|
id: my_light_mode
|
||||||
|
state: rgb
|
||||||
|
- text_sensor.state:
|
||||||
|
id: my_light_mode
|
||||||
|
state: white
|
||||||
|
then:
|
||||||
|
- front_panel.turn_on_leds: [ POWER, COLOR ]
|
||||||
|
- front_panel.set_level: !lambda return id(current_brightness);
|
||||||
|
# In night light mode, turn off the front panel illumination, except
|
||||||
|
# for the power button.
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
- text_sensor.state:
|
||||||
|
id: my_light_mode
|
||||||
|
state: night
|
||||||
|
then:
|
||||||
|
- front_panel.turn_on_leds: [ POWER ]
|
||||||
|
- front_panel.update_leds:
|
||||||
|
|
||||||
|
# This component controls the LED lights of the lamp.
|
||||||
|
light:
|
||||||
|
- platform: xiaomi_bslamp2
|
||||||
|
id: my_light
|
||||||
|
name: ${light_name}
|
||||||
|
default_transition_length: ${default_transition_length}
|
||||||
|
night_calibration:
|
||||||
|
red: ${night_light_red}
|
||||||
|
green: ${night_light_green}
|
||||||
|
blue: ${night_light_blue}
|
||||||
|
|
||||||
|
# When the brightness changes, update the front panel illumination.
|
||||||
|
on_brightness:
|
||||||
|
then:
|
||||||
|
- globals.set:
|
||||||
|
id: current_brightness
|
||||||
|
value: !lambda return x;
|
||||||
|
- script.execute: update_front_panel
|
||||||
|
|
||||||
|
# You can use any effects that you like. These are just examples.
|
||||||
|
effects:
|
||||||
|
- random:
|
||||||
|
name: "Slow Random"
|
||||||
|
transition_length: 30s
|
||||||
|
update_interval: 30s
|
||||||
|
- random:
|
||||||
|
name: "Fast Random"
|
||||||
|
transition_length: 3s
|
||||||
|
update_interval: 3s
|
||||||
|
|
||||||
|
# You can define one or more groups of presets. These presets can
|
||||||
|
# be activated using various "preset.activate" action options.
|
||||||
|
# The presets can for example be used to mimic the behavior of the
|
||||||
|
# original firmware (tapping the color button = go to next preset,
|
||||||
|
# holding the color button = switch between RGB and white light mode).
|
||||||
|
# These bindings have been setup below, using the binary_sensor for
|
||||||
|
# the color button.
|
||||||
|
presets:
|
||||||
|
rgb:
|
||||||
|
red: { red: 100%, green: 0%, blue: 0% }
|
||||||
|
green: { red: 0%, green: 100%, blue: 0% }
|
||||||
|
blue: { red: 0%, green: 0%, blue: 100% }
|
||||||
|
yellow: { red: 100%, green: 100%, blue: 0% }
|
||||||
|
purple: { red: 100%, green: 0%, blue: 100% }
|
||||||
|
randomize: { effect: Fast Random }
|
||||||
|
white:
|
||||||
|
cold: { color_temperature: 153 mireds }
|
||||||
|
chilly: { color_temperature: 275 mireds }
|
||||||
|
luke: { color_temperature: 400 mireds }
|
||||||
|
warm: { color_temperature: 588 mireds }
|
||||||
|
|
||||||
|
# Binary sensors can be created for handling front panel touch / release
|
||||||
|
# events. To specify what part of the front panel to look at, the "for"
|
||||||
|
# parameter can be set to: "POWER_BUTTON", "COLOR_BUTTON" or "SLIDER".
|
||||||
|
binary_sensor:
|
||||||
|
# When tapping the power button, toggle the light.
|
||||||
|
# When holding the power button, turn on night light mode.
|
||||||
|
- platform: xiaomi_bslamp2
|
||||||
|
id: my_power_button
|
||||||
|
for: POWER_BUTTON
|
||||||
|
on_multi_click:
|
||||||
|
- timing:
|
||||||
|
- ON for at most 0.8s
|
||||||
|
then:
|
||||||
|
- light.toggle: my_light
|
||||||
|
- timing:
|
||||||
|
- ON for at least 0.8s
|
||||||
|
then:
|
||||||
|
- light.turn_on:
|
||||||
|
id: my_light
|
||||||
|
brightness: 1%
|
||||||
|
|
||||||
|
# When tapping the color button, activate the next preset.
|
||||||
|
# When holding the color button, activate the next preset group.
|
||||||
|
- platform: xiaomi_bslamp2
|
||||||
|
id: my_color_button
|
||||||
|
for: COLOR_BUTTON
|
||||||
|
on_multi_click:
|
||||||
|
- timing:
|
||||||
|
- ON for at most 0.6s
|
||||||
|
then:
|
||||||
|
- preset.activate:
|
||||||
|
next: preset
|
||||||
|
- timing:
|
||||||
|
- ON for at least 0.6s
|
||||||
|
then:
|
||||||
|
- preset.activate:
|
||||||
|
next: group
|
||||||
|
|
||||||
|
# This sensor component publishes touch events for the front panel slider.
|
||||||
|
# The published value represents the level at which the slider was touched.
|
||||||
|
# By default, values range from 0.01 to 1.00 (in 20 steps). This range can
|
||||||
|
# be modified using the "range_from" and "range_to" parameters.
|
||||||
|
sensor:
|
||||||
|
# When the slider is touched, update the brightness.
|
||||||
|
# Brightness 0.01 initiates the light night mode, which has already
|
||||||
|
# been handled above (by holding the power button). Therefore, brightness
|
||||||
|
# starts from 0.02 here, to not trigger night mode using the slider.
|
||||||
|
- platform: xiaomi_bslamp2
|
||||||
|
id: my_slider_level
|
||||||
|
range_from: 0.02
|
||||||
|
on_value:
|
||||||
|
then:
|
||||||
|
- light.turn_on:
|
||||||
|
id: my_light
|
||||||
|
brightness: !lambda return x;
|
||||||
|
|
||||||
|
# This text sensor propagates the currently active light mode.
|
||||||
|
# The possible light modes are: "off", "rgb", "white" and "night".
|
||||||
|
text_sensor:
|
||||||
|
- platform: xiaomi_bslamp2
|
||||||
|
name: ${light_mode_text_sensor_name}
|
||||||
|
id: my_light_mode
|
||||||
Reference in New Issue
Block a user