You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

303 lines
12 KiB

Introduced a HUB component + front panel IRQ handling A HUB component was introduced. This HUB component has all the knowledge about the Yeelight Bedside Lamp 2 hardware. It known what pins are used, that PWM frequencies to use, what pins to switch in binary mode, etc. etc. No configuration is required for this HUB component. It's automatically loaded when the light component is loaded. The light component will use the HUB component to access the pins that are required for driving the LED circuitry. Note that this simplifies the configuration by A LOT. There's no need anymore to configure the pinouts in the YAML file. This is a logical route to take, since we're talking about a factory-produced PCB with a soldered on ESP32 chip, which uses the same GPIO's and settings on all produced devices (I presume). It would be quite redundant to force every user into configuring these pinouts themselves. ** Beware to update your device yaml configuration ** There are a few pinouts left to move into the HUB. I will do that in the next commit. Your device yaml configuration can be simplified along with these changes. Some of the keys in the existing light configuration block will no longer work and will have to be removed (red, green, blue, white). ** Further development ** The HUB will be extended make it the central component that also handles the I2C communication. This way, there is a central place to regulate the traffic to and from the front panel. We will be able to build upon this by implementing extra, fully separated components that handle for example the front panel light level, the power button, the color button and the slider. ** Interrupt handler for the I2C IRQ trigger pin ** One requirement for the I2C communication has already been implemented: an interrupt handler for the GPIO that is used by the front panel to signal the ESP that a new touch or release event is avilable to be read. It doens't do anything functionally right now, but if you watch the log file, you will see that touch events are detected and that they trigger some log messages.
3 years ago
  1. import esphome.codegen as cg
  2. import esphome.config_validation as cv
  3. from esphome.components import light
  4. from esphome import automation
  5. from esphome.core import Lambda
  6. from esphome.const import (
  7. CONF_RED, CONF_GREEN, CONF_BLUE, CONF_WHITE, CONF_COLOR_TEMPERATURE,
  8. CONF_STATE, CONF_OUTPUT_ID, CONF_TRIGGER_ID, CONF_ID,
  9. CONF_TRANSITION_LENGTH, CONF_BRIGHTNESS, CONF_EFFECT, CONF_FLASH_LENGTH,
  10. CONF_TRANSITIONS, CONF_NAME
  11. )
  12. from .. import bslamp2_ns, CODEOWNERS, CONF_LIGHT_HAL_ID, LightHAL
  13. AUTO_LOAD = ["xiaomi_bslamp2"]
  14. CONF_MASTER1 = "master1"
  15. CONF_MASTER2 = "master2"
  16. CONF_ON_BRIGHTNESS = "on_brightness"
  17. CONF_PRESET_ID = "preset_id"
  18. CONF_PRESETS_ID = "presets_id"
  19. CONF_PRESETS = "presets"
  20. CONF_NEXT = "next"
  21. CONF_GROUP = "group"
  22. CONF_PRESET = "preset"
  23. MIRED_MIN = 153
  24. MIRED_MAX = 588
  25. XiaomiBslamp2LightState = bslamp2_ns.class_("XiaomiBslamp2LightState", light.LightState)
  26. XiaomiBslamp2LightOutput = bslamp2_ns.class_("XiaomiBslamp2LightOutput", light.LightOutput)
  27. XiaomiBslamp2LightTransition = bslamp2_ns.class_("XiaomiBslamp2LightTransition", light.LightTransition)
  28. PresetsContainer = bslamp2_ns.class_("PresetsContainer", cg.Component)
  29. Preset = bslamp2_ns.class_("Preset", cg.Component)
  30. BrightnessTrigger = bslamp2_ns.class_("BrightnessTrigger", automation.Trigger.template())
  31. ActivatePresetAction = bslamp2_ns.class_("ActivatePresetAction", automation.Action)
  32. DiscoAction = bslamp2_ns.class_("DiscoAction", automation.Action)
  33. DiscoAction = bslamp2_ns.class_("DiscoAction", automation.Action)
  34. PRESETS_SCHEMA = cv.Schema({
  35. str.lower: cv.Schema({
  36. str.lower: light.automation.LIGHT_TURN_ON_ACTION_SCHEMA
  37. })
  38. })
  39. def validate_preset(config):
  40. has_rgb = CONF_RED in config or CONF_GREEN in config or CONF_BLUE in config
  41. has_white = CONF_COLOR_TEMPERATURE in config
  42. has_effect = CONF_EFFECT in config
  43. # Check mutual exclusivity of preset options.
  44. if (has_rgb + has_white + has_effect) > 1:
  45. raise cv.Invalid("Use only one of RGB light, white (color temperature) light or an effect")
  46. # Check the color temperature value range.
  47. if has_white:
  48. if config[CONF_COLOR_TEMPERATURE] < MIRED_MIN or config[CONF_COLOR_TEMPERATURE] > MIRED_MAX:
  49. raise cv.Invalid(f"The color temperature must be in the range {MIRED_MIN} - {MIRED_MAX}")
  50. # When defining an RGB color, it is allowed to omit RGB components that have value 0.
  51. if has_rgb:
  52. if CONF_RED not in config:
  53. config[CONF_RED] = 0
  54. if CONF_GREEN not in config:
  55. config[CONF_GREEN] = 0
  56. if CONF_BLUE not in config:
  57. config[CONF_BLUE] = 0
  58. return config
  59. PRESET_SCHEMA = cv.All(
  60. cv.Schema(
  61. {
  62. cv.GenerateID(CONF_ID): cv.use_id(XiaomiBslamp2LightState),
  63. cv.GenerateID(CONF_PRESET_ID): cv.declare_id(Preset),
  64. cv.Optional(CONF_EFFECT): cv.string,
  65. cv.Optional(CONF_COLOR_TEMPERATURE): cv.color_temperature,
  66. cv.Optional(CONF_RED): cv.percentage,
  67. cv.Optional(CONF_GREEN): cv.percentage,
  68. cv.Optional(CONF_BLUE): cv.percentage,
  69. cv.Optional(CONF_BRIGHTNESS): cv.percentage,
  70. cv.Optional(CONF_TRANSITION_LENGTH): cv.positive_time_period_milliseconds,
  71. }
  72. ),
  73. validate_preset
  74. )
  75. CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
  76. {
  77. cv.GenerateID(CONF_ID): cv.declare_id(XiaomiBslamp2LightState),
  78. cv.GenerateID(CONF_LIGHT_HAL_ID): cv.use_id(LightHAL),
  79. cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(XiaomiBslamp2LightOutput),
  80. cv.Optional(
  81. CONF_TRANSITIONS, default=["bslamp2"]
  82. ): light.transitions.validate_transitions(XiaomiBslamp2LightOutput),
  83. cv.Optional(CONF_ON_BRIGHTNESS): automation.validate_automation(
  84. {
  85. cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BrightnessTrigger),
  86. }
  87. ),
  88. cv.GenerateID(CONF_PRESETS_ID): cv.declare_id(PresetsContainer),
  89. cv.Optional(CONF_PRESETS): cv.Schema({
  90. str.lower: cv.Schema({
  91. str.lower: PRESET_SCHEMA
  92. })
  93. }),
  94. }
  95. )
  96. def is_preset_group(value):
  97. return value
  98. def is_preset(value):
  99. return value
  100. def maybe_simple_preset_action(schema):
  101. def validator(value):
  102. if isinstance(value, dict):
  103. return schema(value)
  104. value = value.lower()
  105. config = {}
  106. if value == "next_group":
  107. config[CONF_NEXT] = CONF_GROUP
  108. elif value == "next_preset":
  109. config[CONF_NEXT] = CONF_PRESET
  110. elif "." not in value:
  111. config[CONF_GROUP] = value
  112. else:
  113. group, preset = value.split(".", 2)
  114. config[CONF_GROUP] = group
  115. config[CONF_PRESET] = preset
  116. return schema(config)
  117. return validator
  118. @automation.register_action(
  119. "light.disco_on", DiscoAction, light.automation.LIGHT_TURN_ON_ACTION_SCHEMA
  120. )
  121. async def disco_action_on_to_code(config, action_id, template_arg, args):
  122. light_var = await cg.get_variable(config[CONF_ID])
  123. var = cg.new_Pvariable(action_id, template_arg, light_var)
  124. if CONF_STATE in config:
  125. template_ = await cg.templatable(config[CONF_STATE], args, bool)
  126. cg.add(var.set_state(template_))
  127. if CONF_TRANSITION_LENGTH in config:
  128. template_ = await cg.templatable(
  129. config[CONF_TRANSITION_LENGTH], args, cg.uint32
  130. )
  131. cg.add(var.set_transition_length(template_))
  132. if CONF_FLASH_LENGTH in config:
  133. template_ = await cg.templatable(config[CONF_FLASH_LENGTH], args, cg.uint32)
  134. cg.add(var.set_flash_length(template_))
  135. if CONF_BRIGHTNESS in config:
  136. template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, float)
  137. cg.add(var.set_brightness(template_))
  138. if CONF_RED in config:
  139. template_ = await cg.templatable(config[CONF_RED], args, float)
  140. cg.add(var.set_red(template_))
  141. if CONF_GREEN in config:
  142. template_ = await cg.templatable(config[CONF_GREEN], args, float)
  143. cg.add(var.set_green(template_))
  144. if CONF_BLUE in config:
  145. template_ = await cg.templatable(config[CONF_BLUE], args, float)
  146. cg.add(var.set_blue(template_))
  147. if CONF_COLOR_TEMPERATURE in config:
  148. template_ = await cg.templatable(config[CONF_COLOR_TEMPERATURE], args, float)
  149. cg.add(var.set_color_temperature(template_))
  150. if CONF_EFFECT in config:
  151. template_ = await cg.templatable(config[CONF_EFFECT], args, cg.std_string)
  152. cg.add(var.set_effect(template_))
  153. return var
  154. @automation.register_action(
  155. "light.disco_off", DiscoAction, light.automation.LIGHT_TURN_OFF_ACTION_SCHEMA
  156. )
  157. async def disco_action_off_to_code(config, action_id, template_arg, args):
  158. light_var = await cg.get_variable(config[CONF_ID])
  159. var = cg.new_Pvariable(action_id, template_arg, light_var)
  160. cg.add(var.set_disco_state(False))
  161. return var
  162. USED_PRESETS = []
  163. def register_preset_action(value):
  164. if "group" in value and not isinstance(value["group"], Lambda):
  165. if "preset" in value and not isinstance(value["preset"], Lambda):
  166. preset_data = [value['group'], value['preset']]
  167. else:
  168. preset_data = [value["group"], None]
  169. USED_PRESETS.append(preset_data)
  170. return value
  171. @automation.register_action(
  172. "preset.activate",
  173. ActivatePresetAction,
  174. cv.All(
  175. maybe_simple_preset_action(cv.Any(
  176. cv.Schema({
  177. cv.GenerateID(CONF_PRESETS_ID): cv.use_id(PresetsContainer),
  178. cv.Required(CONF_GROUP): cv.templatable(cv.string),
  179. cv.Optional(CONF_PRESET): cv.templatable(cv.string)
  180. }),
  181. cv.Schema({
  182. cv.GenerateID(CONF_PRESETS_ID): cv.use_id(PresetsContainer),
  183. cv.Required(CONF_NEXT): cv.one_of(CONF_GROUP, CONF_PRESET, lower=True)
  184. })
  185. )),
  186. register_preset_action
  187. ),
  188. )
  189. async def preset_activate_to_code(config, action_id, template_arg, args):
  190. presets_var = await cg.get_variable(config[CONF_PRESETS_ID])
  191. action_var = cg.new_Pvariable(action_id, template_arg, presets_var)
  192. if CONF_NEXT in config:
  193. cg.add(action_var.set_operation(f"next_{config[CONF_NEXT]}"))
  194. elif CONF_PRESET in config:
  195. cg.add(action_var.set_operation("activate_preset"))
  196. group_template_ = await cg.templatable(config[CONF_GROUP], args, cg.std_string)
  197. cg.add(action_var.set_group(group_template_))
  198. preset_template_ = await cg.templatable(config[CONF_PRESET], args, cg.std_string)
  199. cg.add(action_var.set_preset(preset_template_))
  200. else:
  201. cg.add(action_var.set_operation("activate_group"))
  202. group_template_ = await cg.templatable(config[CONF_GROUP], args, cg.std_string)
  203. cg.add(action_var.set_group(group_template_))
  204. return action_var
  205. @light.transitions.register_output_transition(
  206. XiaomiBslamp2LightOutput,
  207. "bslamp2",
  208. XiaomiBslamp2LightTransition,
  209. "bslamp2",
  210. {
  211. cv.GenerateID(CONF_LIGHT_HAL_ID): cv.use_id(LightHAL),
  212. },
  213. )
  214. async def bslamp2_transition_to_code(config, transition_id):
  215. light_hal_var = await cg.get_variable(config[CONF_LIGHT_HAL_ID])
  216. return cg.new_Pvariable(
  217. transition_id,
  218. config[CONF_NAME],
  219. light_hal_var
  220. )
  221. async def light_output_to_code(config):
  222. light_output_var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
  223. await light.register_light(light_output_var, config)
  224. light_hal_var = await cg.get_variable(config[CONF_LIGHT_HAL_ID])
  225. cg.add(light_output_var.set_parent(light_hal_var))
  226. async def on_brightness_to_code(config):
  227. light_hal_var = await cg.get_variable(config[CONF_LIGHT_HAL_ID])
  228. for config in config.get(CONF_ON_BRIGHTNESS, []):
  229. trigger = cg.new_Pvariable(config[CONF_TRIGGER_ID], light_hal_var)
  230. await automation.build_automation(trigger, [(float, "x")], config)
  231. async def preset_to_code(config, preset_group, preset_name):
  232. light_var = await cg.get_variable(config[CONF_ID])
  233. preset_var = cg.new_Pvariable(
  234. config[CONF_PRESET_ID], light_var, preset_group, preset_name)
  235. if CONF_TRANSITION_LENGTH in config:
  236. cg.add(preset_var.set_transition_length(config[CONF_TRANSITION_LENGTH]))
  237. if CONF_BRIGHTNESS in config:
  238. cg.add(preset_var.set_brightness(config[CONF_BRIGHTNESS]))
  239. if CONF_RED in config:
  240. cg.add(preset_var.set_red(config[CONF_RED]))
  241. if CONF_GREEN in config:
  242. cg.add(preset_var.set_green(config[CONF_GREEN]))
  243. if CONF_BLUE in config:
  244. cg.add(preset_var.set_blue(config[CONF_BLUE]))
  245. if CONF_COLOR_TEMPERATURE in config:
  246. cg.add(preset_var.set_color_temperature(config[CONF_COLOR_TEMPERATURE]))
  247. if CONF_EFFECT in config:
  248. cg.add(preset_var.set_effect(config[CONF_EFFECT]))
  249. else:
  250. cg.add(preset_var.set_effect("None"))
  251. return await cg.register_component(preset_var, config)
  252. async def presets_to_code(config):
  253. presets_var = cg.new_Pvariable(config[CONF_PRESETS_ID])
  254. await cg.register_component(presets_var, config)
  255. for preset_group, presets in config.get(CONF_PRESETS, {}).items():
  256. for preset_name, preset_config in presets.items():
  257. preset = await preset_to_code(preset_config, preset_group, preset_name)
  258. cg.add(presets_var.add_preset(preset))
  259. async def to_code(config):
  260. await light_output_to_code(config)
  261. await on_brightness_to_code(config)
  262. await presets_to_code(config)
  263. def validate(config):
  264. valid_presets = config.get(CONF_PRESETS, {});
  265. for group, preset in USED_PRESETS:
  266. if group not in valid_presets:
  267. raise cv.Invalid(f"Invalid light preset group '{group}' used")
  268. if preset is not None and preset not in valid_presets[group]:
  269. raise cv.Invalid(f"Invalid light preset '{group}.{preset}' used")
  270. return config
  271. FINAL_VALIDATE_SCHEMA = cv.Schema(validate);