diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 433e5d2792..ab4f8cc960 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,56 +1,64 @@ { "name": "ESPHome Dev", - "image": "esphome/esphome-lint:dev", + "image": "ghcr.io/esphome/esphome-lint:dev", "postCreateCommand": [ "script/devcontainer-post-create" ], + "containerEnv": { + "DEVCONTAINER": "1" + }, "runArgs": [ "--privileged", "-e", "ESPHOME_DASHBOARD_USE_PING=1" ], "appPort": 6052, - "extensions": [ - // python - "ms-python.python", - "visualstudioexptteam.vscodeintellicode", - // yaml - "redhat.vscode-yaml", - // cpp - "ms-vscode.cpptools", - // editorconfig - "editorconfig.editorconfig", - ], - "settings": { - "python.languageServer": "Pylance", - "python.pythonPath": "/usr/bin/python3", - "python.linting.pylintEnabled": true, - "python.linting.enabled": true, - "python.formatting.provider": "black", - "editor.formatOnPaste": false, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "files.trimTrailingWhitespace": true, - "terminal.integrated.defaultProfile.linux": "bash", - "yaml.customTags": [ - "!secret scalar", - "!lambda scalar", - "!include_dir_named scalar", - "!include_dir_list scalar", - "!include_dir_merge_list scalar", - "!include_dir_merge_named scalar" - ], - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/*.pyc": { - "when": "$(basename).py" - }, - "**/__pycache__": true - }, - "files.associations": { - "**/.vscode/*.json": "jsonc" - }, - "C_Cpp.clang_format_path": "/usr/bin/clang-format-11", + "customizations": { + "vscode": { + "extensions": [ + // python + "ms-python.python", + "visualstudioexptteam.vscodeintellicode", + // yaml + "redhat.vscode-yaml", + // cpp + "ms-vscode.cpptools", + // editorconfig + "editorconfig.editorconfig", + ], + "settings": { + "python.languageServer": "Pylance", + "python.pythonPath": "/usr/bin/python3", + "python.linting.pylintEnabled": true, + "python.linting.enabled": true, + "python.formatting.provider": "black", + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "files.trimTrailingWhitespace": true, + "terminal.integrated.defaultProfile.linux": "bash", + "yaml.customTags": [ + "!secret scalar", + "!lambda scalar", + "!extend scalar", + "!include_dir_named scalar", + "!include_dir_list scalar", + "!include_dir_merge_list scalar", + "!include_dir_merge_named scalar" + ], + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": { + "when": "$(basename).py" + }, + "**/__pycache__": true + }, + "files.associations": { + "**/.vscode/*.json": "jsonc" + }, + "C_Cpp.clang_format_path": "/usr/bin/clang-format-13" + } + } } } diff --git a/.editorconfig b/.editorconfig index 8ccf1eeebc..9e203f60e4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -25,10 +25,9 @@ indent_size = 2 [*.{yaml,yml}] indent_style = space indent_size = 2 -quote_type = single +quote_type = double # JSON [*.json] indent_style = space indent_size = 2 - diff --git a/.gitattributes b/.gitattributes index dad0966222..1b3fd332b4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ # Normalize line endings to LF in the repository * text eol=lf +*.png binary diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 864586fe6b..a8ca63d158 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,4 @@ +--- # These are supported funding model platforms custom: https://www.nabucasa.com diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 4add58dfbe..804dad47c7 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,3 +1,4 @@ +--- blank_issues_enabled: false contact_links: - name: Issue Tracker @@ -5,8 +6,10 @@ contact_links: about: Please create bug reports in the dedicated issue tracker. - name: Feature Request Tracker url: https://github.com/esphome/feature-requests - about: Please create feature requests in the dedicated feature request tracker. + about: | + Please create feature requests in the dedicated feature request tracker. - name: Frequently Asked Question url: https://esphome.io/guides/faq.html - about: Please view the FAQ for common questions and what to include in a bug report. - + about: | + Please view the FAQ for common questions and what + to include in a bug report. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 25411c19f5..3221b8ac5c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ -# What does this implement/fix? +# What does this implement/fix? -Quick description and explanation of changes + ## Types of changes @@ -18,6 +18,7 @@ Quick description and explanation of changes - [ ] ESP32 - [ ] ESP32 IDF - [ ] ESP8266 +- [ ] RP2040 ## Example entry for `config.yaml`: - - - - - - {{ App.get_name() }} - - - - -
-

WiFi Networks

-
- The ESP will now try to connect to the network...
- Please give it some time to connect.
- Note: Copy the changed network to your YAML file - the next OTA update will overwrite these settings. -
-
- - - AP1 - - -
-
- - - AP2 - -
- -

WiFi Settings

-
-
-
-
- -
-

-
- -

OTA Update

-
- - -
-
- - diff --git a/esphome/components/captive_portal/lock.svg b/esphome/components/captive_portal/lock.svg deleted file mode 100644 index 743a1cc55a..0000000000 --- a/esphome/components/captive_portal/lock.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/esphome/components/captive_portal/stylesheet.css b/esphome/components/captive_portal/stylesheet.css deleted file mode 100644 index 73f82f05f1..0000000000 --- a/esphome/components/captive_portal/stylesheet.css +++ /dev/null @@ -1,58 +0,0 @@ -* { - box-sizing: inherit; -} - -div, input { - padding: 5px; - font-size: 1em; -} - -input { - width: 95%; -} - -body { - text-align: center; - font-family: sans-serif; -} - -button { - border: 0; - border-radius: 0.3rem; - background-color: #1fa3ec; - color: #fff; - line-height: 2.4rem; - font-size: 1.2rem; - width: 100%; - padding: 0; -} - -.main { - text-align: left; - display: inline-block; - min-width: 260px; -} - -.network { - display: flex; - justify-content: space-between; - align-items: center; -} - -.network-left { - display: flex; - align-items: center; -} - -.network-ssid { - margin-bottom: -7px; - margin-left: 10px; -} - -.info { - border: 1px solid; - margin: 10px 0px; - padding: 15px 10px; - color: #4f8a10; - background-color: #dff2bf; -} diff --git a/esphome/components/captive_portal/wifi-strength-1.svg b/esphome/components/captive_portal/wifi-strength-1.svg deleted file mode 100644 index 189a38193c..0000000000 --- a/esphome/components/captive_portal/wifi-strength-1.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/esphome/components/captive_portal/wifi-strength-2.svg b/esphome/components/captive_portal/wifi-strength-2.svg deleted file mode 100644 index 9b4b2d2396..0000000000 --- a/esphome/components/captive_portal/wifi-strength-2.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/esphome/components/captive_portal/wifi-strength-3.svg b/esphome/components/captive_portal/wifi-strength-3.svg deleted file mode 100644 index 44b7532bb7..0000000000 --- a/esphome/components/captive_portal/wifi-strength-3.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/esphome/components/captive_portal/wifi-strength-4.svg b/esphome/components/captive_portal/wifi-strength-4.svg deleted file mode 100644 index a22b0b8281..0000000000 --- a/esphome/components/captive_portal/wifi-strength-4.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/esphome/components/ccs811/ccs811.cpp b/esphome/components/ccs811/ccs811.cpp index 5c60989afa..f1dadf673a 100644 --- a/esphome/components/ccs811/ccs811.cpp +++ b/esphome/components/ccs811/ccs811.cpp @@ -145,8 +145,8 @@ void CCS811Component::send_env_data_() { // https://github.com/adafruit/Adafruit_CCS811/blob/0990f5c620354d8bc087c4706bec091d8e6e5dfd/Adafruit_CCS811.cpp#L135-L142 uint16_t hum_conv = static_cast(lroundf(humidity * 512.0f + 0.5f)); uint16_t temp_conv = static_cast(lroundf(temperature * 512.0f + 0.5f)); - this->write_bytes(0x05, {(uint8_t)((hum_conv >> 8) & 0xff), (uint8_t)((hum_conv & 0xff)), - (uint8_t)((temp_conv >> 8) & 0xff), (uint8_t)((temp_conv & 0xff))}); + this->write_bytes(0x05, {(uint8_t) ((hum_conv >> 8) & 0xff), (uint8_t) ((hum_conv & 0xff)), + (uint8_t) ((temp_conv >> 8) & 0xff), (uint8_t) ((temp_conv & 0xff))}); } void CCS811Component::dump_config() { ESP_LOGCONFIG(TAG, "CCS811"); diff --git a/esphome/components/ccs811/sensor.py b/esphome/components/ccs811/sensor.py index bb8200273d..af3e6574ab 100644 --- a/esphome/components/ccs811/sensor.py +++ b/esphome/components/ccs811/sensor.py @@ -2,12 +2,11 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor, text_sensor from esphome.const import ( - CONF_ICON, CONF_ID, ICON_RADIATOR, ICON_RESTART, DEVICE_CLASS_CARBON_DIOXIDE, - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, UNIT_PARTS_PER_BILLION, @@ -44,14 +43,11 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_PARTS_PER_BILLION, icon=ICON_RADIATOR, accuracy_decimals=0, - device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Optional(CONF_VERSION): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - cv.Optional(CONF_ICON, default=ICON_RESTART): cv.icon, - } + cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema( + icon=ICON_RESTART ), cv.Optional(CONF_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_TEMPERATURE): cv.use_id(sensor.Sensor), @@ -74,8 +70,7 @@ async def to_code(config): cg.add(var.set_tvoc(sens)) if CONF_VERSION in config: - sens = cg.new_Pvariable(config[CONF_VERSION][CONF_ID]) - await text_sensor.register_text_sensor(sens, config[CONF_VERSION]) + sens = await text_sensor.new_text_sensor(config[CONF_VERSION]) cg.add(var.set_version(sens)) if CONF_BASELINE in config: diff --git a/esphome/components/cd74hc4067/__init__.py b/esphome/components/cd74hc4067/__init__.py index 4fb15d1bf3..d57061b710 100644 --- a/esphome/components/cd74hc4067/__init__.py +++ b/esphome/components/cd74hc4067/__init__.py @@ -27,10 +27,10 @@ DEFAULT_DELAY = "2ms" CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(CD74HC4067Component), - cv.Required(CONF_PIN_S0): pins.internal_gpio_output_pin_schema, - cv.Required(CONF_PIN_S1): pins.internal_gpio_output_pin_schema, - cv.Required(CONF_PIN_S2): pins.internal_gpio_output_pin_schema, - cv.Required(CONF_PIN_S3): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_PIN_S0): pins.gpio_output_pin_schema, + cv.Required(CONF_PIN_S1): pins.gpio_output_pin_schema, + cv.Required(CONF_PIN_S2): pins.gpio_output_pin_schema, + cv.Required(CONF_PIN_S3): pins.gpio_output_pin_schema, cv.Optional( CONF_DELAY, default=DEFAULT_DELAY ): cv.positive_time_period_milliseconds, diff --git a/esphome/components/cd74hc4067/cd74hc4067.h b/esphome/components/cd74hc4067/cd74hc4067.h index 4a5c2e4e35..6193513575 100644 --- a/esphome/components/cd74hc4067/cd74hc4067.h +++ b/esphome/components/cd74hc4067/cd74hc4067.h @@ -19,22 +19,22 @@ class CD74HC4067Component : public Component { void activate_pin(uint8_t pin); /// set the pin connected to multiplexer control pin 0 - void set_pin_s0(InternalGPIOPin *pin) { this->pin_s0_ = pin; } + void set_pin_s0(GPIOPin *pin) { this->pin_s0_ = pin; } /// set the pin connected to multiplexer control pin 1 - void set_pin_s1(InternalGPIOPin *pin) { this->pin_s1_ = pin; } + void set_pin_s1(GPIOPin *pin) { this->pin_s1_ = pin; } /// set the pin connected to multiplexer control pin 2 - void set_pin_s2(InternalGPIOPin *pin) { this->pin_s2_ = pin; } + void set_pin_s2(GPIOPin *pin) { this->pin_s2_ = pin; } /// set the pin connected to multiplexer control pin 3 - void set_pin_s3(InternalGPIOPin *pin) { this->pin_s3_ = pin; } + void set_pin_s3(GPIOPin *pin) { this->pin_s3_ = pin; } /// set the delay needed after an input switch void set_switch_delay(uint32_t switch_delay) { this->switch_delay_ = switch_delay; } private: - InternalGPIOPin *pin_s0_; - InternalGPIOPin *pin_s1_; - InternalGPIOPin *pin_s2_; - InternalGPIOPin *pin_s3_; + GPIOPin *pin_s0_; + GPIOPin *pin_s1_; + GPIOPin *pin_s2_; + GPIOPin *pin_s3_; /// the currently active pin uint8_t active_pin_; uint32_t switch_delay_; diff --git a/esphome/components/cd74hc4067/sensor.py b/esphome/components/cd74hc4067/sensor.py index 7c7cf9ccb7..3eee34b85e 100644 --- a/esphome/components/cd74hc4067/sensor.py +++ b/esphome/components/cd74hc4067/sensor.py @@ -25,6 +25,7 @@ CONF_CD74HC4067_ID = "cd74hc4067_id" CONFIG_SCHEMA = ( sensor.sensor_schema( + CD74HC4067Sensor, unit_of_measurement=UNIT_VOLT, accuracy_decimals=3, device_class=DEVICE_CLASS_VOLTAGE, @@ -33,7 +34,6 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.GenerateID(): cv.declare_id(CD74HC4067Sensor), cv.GenerateID(CONF_CD74HC4067_ID): cv.use_id(CD74HC4067Component), cv.Required(CONF_NUMBER): cv.int_range(0, 15), cv.Required(CONF_SENSOR): cv.use_id(voltage_sampler.VoltageSampler), @@ -47,8 +47,8 @@ async def to_code(config): parent = await cg.get_variable(config[CONF_CD74HC4067_ID]) var = cg.new_Pvariable(config[CONF_ID], parent) - await cg.register_component(var, config) await sensor.register_sensor(var, config) + await cg.register_component(var, config) cg.add(var.set_pin(config[CONF_NUMBER])) sens = await cg.get_variable(config[CONF_SENSOR]) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 87b9a4b3e2..bf167fe837 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -20,8 +20,11 @@ from esphome.const import ( CONF_MODE, CONF_MODE_COMMAND_TOPIC, CONF_MODE_STATE_TOPIC, + CONF_ON_CONTROL, CONF_ON_STATE, CONF_PRESET, + CONF_PRESET_COMMAND_TOPIC, + CONF_PRESET_STATE_TOPIC, CONF_SWING_MODE, CONF_SWING_MODE_COMMAND_TOPIC, CONF_SWING_MODE_STATE_TOPIC, @@ -73,6 +76,7 @@ CLIMATE_FAN_MODES = { "MIDDLE": ClimateFanMode.CLIMATE_FAN_MIDDLE, "FOCUS": ClimateFanMode.CLIMATE_FAN_FOCUS, "DIFFUSE": ClimateFanMode.CLIMATE_FAN_DIFFUSE, + "QUIET": ClimateFanMode.CLIMATE_FAN_QUIET, } validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True) @@ -101,9 +105,40 @@ CLIMATE_SWING_MODES = { validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) +CONF_CURRENT_TEMPERATURE = "current_temperature" + +visual_temperature = cv.float_with_unit( + "visual_temperature", "(°C|° C|°|C|° K|° K|K|°F|° F|F)?" +) + + +def single_visual_temperature(value): + if isinstance(value, dict): + return value + + value = visual_temperature(value) + return VISUAL_TEMPERATURE_STEP_SCHEMA( + { + CONF_TARGET_TEMPERATURE: value, + CONF_CURRENT_TEMPERATURE: value, + } + ) + + # Actions ControlAction = climate_ns.class_("ControlAction", automation.Action) StateTrigger = climate_ns.class_("StateTrigger", automation.Trigger.template()) +ControlTrigger = climate_ns.class_("ControlTrigger", automation.Trigger.template()) + +VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Any( + single_visual_temperature, + cv.Schema( + { + cv.Required(CONF_TARGET_TEMPERATURE): visual_temperature, + cv.Required(CONF_CURRENT_TEMPERATURE): visual_temperature, + } + ), +) CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { @@ -113,7 +148,7 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). { cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature, cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature, - cv.Optional(CONF_TEMPERATURE_STEP): cv.temperature, + cv.Optional(CONF_TEMPERATURE_STEP): VISUAL_TEMPERATURE_STEP_SCHEMA, } ), cv.Optional(CONF_ACTION_STATE_TOPIC): cv.All( @@ -140,6 +175,12 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). cv.Optional(CONF_MODE_STATE_TOPIC): cv.All( cv.requires_component("mqtt"), cv.publish_topic ), + cv.Optional(CONF_PRESET_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_PRESET_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), cv.Optional(CONF_SWING_MODE_COMMAND_TOPIC): cv.All( cv.requires_component("mqtt"), cv.publish_topic ), @@ -164,6 +205,11 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). cv.Optional(CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC): cv.All( cv.requires_component("mqtt"), cv.publish_topic ), + cv.Optional(CONF_ON_CONTROL): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ControlTrigger), + } + ), cv.Optional(CONF_ON_STATE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), @@ -182,7 +228,12 @@ async def setup_climate_core_(var, config): if CONF_MAX_TEMPERATURE in visual: cg.add(var.set_visual_max_temperature_override(visual[CONF_MAX_TEMPERATURE])) if CONF_TEMPERATURE_STEP in visual: - cg.add(var.set_visual_temperature_step_override(visual[CONF_TEMPERATURE_STEP])) + cg.add( + var.set_visual_temperature_step_override( + visual[CONF_TEMPERATURE_STEP][CONF_TARGET_TEMPERATURE], + visual[CONF_TEMPERATURE_STEP][CONF_CURRENT_TEMPERATURE], + ) + ) if CONF_MQTT_ID in config: mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) @@ -214,7 +265,12 @@ async def setup_climate_core_(var, config): cg.add(mqtt_.set_custom_mode_command_topic(config[CONF_MODE_COMMAND_TOPIC])) if CONF_MODE_STATE_TOPIC in config: cg.add(mqtt_.set_custom_mode_state_topic(config[CONF_MODE_STATE_TOPIC])) - + if CONF_PRESET_COMMAND_TOPIC in config: + cg.add( + mqtt_.set_custom_preset_command_topic(config[CONF_PRESET_COMMAND_TOPIC]) + ) + if CONF_PRESET_STATE_TOPIC in config: + cg.add(mqtt_.set_custom_preset_state_topic(config[CONF_PRESET_STATE_TOPIC])) if CONF_SWING_MODE_COMMAND_TOPIC in config: cg.add( mqtt_.set_custom_swing_mode_command_topic( @@ -268,6 +324,10 @@ async def setup_climate_core_(var, config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_CONTROL, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + async def register_climate(var, config): if not CORE.has_id(config[CONF_ID]): @@ -283,13 +343,15 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature), cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature), - cv.Optional(CONF_AWAY): cv.templatable(cv.boolean), + cv.Optional(CONF_AWAY): cv.invalid("Use preset instead"), cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable( validate_climate_fan_mode ), - cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.string_strict, + cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.templatable( + cv.string_strict + ), cv.Exclusive(CONF_PRESET, "preset"): cv.templatable(validate_climate_preset), - cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.string_strict, + cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.templatable(cv.string_strict), cv.Optional(CONF_SWING_MODE): cv.templatable(validate_climate_swing_mode), } ) @@ -317,20 +379,21 @@ async def climate_control_to_code(config, action_id, template_arg, args): config[CONF_TARGET_TEMPERATURE_HIGH], args, float ) cg.add(var.set_target_temperature_high(template_)) - if CONF_AWAY in config: - template_ = await cg.templatable(config[CONF_AWAY], args, bool) - cg.add(var.set_away(template_)) if CONF_FAN_MODE in config: template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode) cg.add(var.set_fan_mode(template_)) if CONF_CUSTOM_FAN_MODE in config: - template_ = await cg.templatable(config[CONF_CUSTOM_FAN_MODE], args, str) + template_ = await cg.templatable( + config[CONF_CUSTOM_FAN_MODE], args, cg.std_string + ) cg.add(var.set_custom_fan_mode(template_)) if CONF_PRESET in config: template_ = await cg.templatable(config[CONF_PRESET], args, ClimatePreset) cg.add(var.set_preset(template_)) if CONF_CUSTOM_PRESET in config: - template_ = await cg.templatable(config[CONF_CUSTOM_PRESET], args, str) + template_ = await cg.templatable( + config[CONF_CUSTOM_PRESET], args, cg.std_string + ) cg.add(var.set_custom_preset(template_)) if CONF_SWING_MODE in config: template_ = await cg.templatable( diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index 3145358dab..9b06563eb4 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -42,6 +42,13 @@ template class ControlAction : public Action { Climate *climate_; }; +class ControlTrigger : public Trigger<> { + public: + ControlTrigger(Climate *climate) { + climate->add_on_control_callback([this]() { this->trigger(); }); + } +}; + class StateTrigger : public Trigger<> { public: StateTrigger(Climate *climate) { diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index ebea20ed1f..a032596eb3 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -1,4 +1,5 @@ #include "climate.h" +#include "esphome/core/macros.h" namespace esphome { namespace climate { @@ -43,6 +44,7 @@ void ClimateCall::perform() { if (this->target_temperature_high_.has_value()) { ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_); } + this->parent_->control_callback_.call(); this->parent_->control(*this); } void ClimateCall::validate_() { @@ -173,6 +175,8 @@ ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { this->set_fan_mode(CLIMATE_FAN_FOCUS); } else if (str_equals_case_insensitive(fan_mode, "DIFFUSE")) { this->set_fan_mode(CLIMATE_FAN_DIFFUSE); + } else if (str_equals_case_insensitive(fan_mode, "QUIET")) { + this->set_fan_mode(CLIMATE_FAN_QUIET); } else { if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) { this->custom_fan_mode_ = fan_mode; @@ -260,25 +264,11 @@ const optional &ClimateCall::get_mode() const { return this->mode_; const optional &ClimateCall::get_target_temperature() const { return this->target_temperature_; } const optional &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; } const optional &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; } -optional ClimateCall::get_away() const { - if (!this->preset_.has_value()) - return {}; - return *this->preset_ == ClimatePreset::CLIMATE_PRESET_AWAY; -} const optional &ClimateCall::get_fan_mode() const { return this->fan_mode_; } const optional &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } const optional &ClimateCall::get_preset() const { return this->preset_; } const optional &ClimateCall::get_custom_preset() const { return this->custom_preset_; } const optional &ClimateCall::get_swing_mode() const { return this->swing_mode_; } -ClimateCall &ClimateCall::set_away(bool away) { - this->preset_ = away ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME; - return *this; -} -ClimateCall &ClimateCall::set_away(optional away) { - if (away.has_value()) - this->preset_ = *away ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME; - return *this; -} ClimateCall &ClimateCall::set_target_temperature_high(optional target_temperature_high) { this->target_temperature_high_ = target_temperature_high; return *this; @@ -314,6 +304,10 @@ void Climate::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } +void Climate::add_on_control_callback(std::function &&callback) { + this->control_callback_.add(std::move(callback)); +} + // Random 32bit value; If this changes existing restore preferences are invalidated static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL; @@ -326,14 +320,17 @@ optional Climate::restore_state_() { return recovered; } void Climate::save_state_() { -#if defined(USE_ESP_IDF) && !defined(CLANG_TIDY) +#if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \ + !defined(CLANG_TIDY) #pragma GCC diagnostic ignored "-Wclass-memaccess" +#define TEMP_IGNORE_MEMACCESS #endif ClimateDeviceRestoreState state{}; // initialize as zero to prevent random data on stack triggering erase memset(&state, 0, sizeof(ClimateDeviceRestoreState)); -#if USE_ESP_IDF && !defined(CLANG_TIDY) +#ifdef TEMP_IGNORE_MEMACCESS #pragma GCC diagnostic pop +#undef TEMP_IGNORE_MEMACCESS #endif state.mode = this->mode; @@ -415,7 +412,6 @@ void Climate::publish_state() { // Save state this->save_state_(); } -uint32_t Climate::hash_base() { return 3104134496UL; } ClimateTraits Climate::get_traits() { auto traits = this->traits(); @@ -425,9 +421,11 @@ ClimateTraits Climate::get_traits() { if (this->visual_max_temperature_override_.has_value()) { traits.set_visual_max_temperature(*this->visual_max_temperature_override_); } - if (this->visual_temperature_step_override_.has_value()) { - traits.set_visual_temperature_step(*this->visual_temperature_step_override_); + if (this->visual_target_temperature_step_override_.has_value()) { + traits.set_visual_target_temperature_step(*this->visual_target_temperature_step_override_); + traits.set_visual_current_temperature_step(*this->visual_current_temperature_step_override_); } + return traits; } @@ -437,15 +435,11 @@ void Climate::set_visual_min_temperature_override(float visual_min_temperature_o void Climate::set_visual_max_temperature_override(float visual_max_temperature_override) { this->visual_max_temperature_override_ = visual_max_temperature_override; } -void Climate::set_visual_temperature_step_override(float visual_temperature_step_override) { - this->visual_temperature_step_override_ = visual_temperature_step_override; +void Climate::set_visual_temperature_step_override(float target, float current) { + this->visual_target_temperature_step_override_ = target; + this->visual_current_temperature_step_override_ = current; } -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" -Climate::Climate(const std::string &name) : EntityBase(name) {} -#pragma GCC diagnostic pop -Climate::Climate() : Climate("") {} ClimateCall Climate::make_call() { return ClimateCall(this); } ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { @@ -536,13 +530,18 @@ void Climate::dump_traits_(const char *tag) { ESP_LOGCONFIG(tag, " [x] Visual settings:"); ESP_LOGCONFIG(tag, " - Min: %.1f", traits.get_visual_min_temperature()); ESP_LOGCONFIG(tag, " - Max: %.1f", traits.get_visual_max_temperature()); - ESP_LOGCONFIG(tag, " - Step: %.1f", traits.get_visual_temperature_step()); - if (traits.get_supports_current_temperature()) + ESP_LOGCONFIG(tag, " - Step:"); + ESP_LOGCONFIG(tag, " Target: %.1f", traits.get_visual_target_temperature_step()); + ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step()); + if (traits.get_supports_current_temperature()) { ESP_LOGCONFIG(tag, " [x] Supports current temperature"); - if (traits.get_supports_two_point_target_temperature()) + } + if (traits.get_supports_two_point_target_temperature()) { ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature"); - if (traits.get_supports_action()) + } + if (traits.get_supports_action()) { ESP_LOGCONFIG(tag, " [x] Supports action"); + } if (!traits.get_supported_modes().empty()) { ESP_LOGCONFIG(tag, " [x] Supported modes:"); for (ClimateMode m : traits.get_supported_modes()) diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 852b76686c..656e1c4852 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -64,10 +64,6 @@ class ClimateCall { * For climate devices with two point target temperature control */ ClimateCall &set_target_temperature_high(optional target_temperature_high); - ESPDEPRECATED("set_away() is deprecated, please use .set_preset(CLIMATE_PRESET_AWAY) instead", "v1.20") - ClimateCall &set_away(bool away); - ESPDEPRECATED("set_away() is deprecated, please use .set_preset(CLIMATE_PRESET_AWAY) instead", "v1.20") - ClimateCall &set_away(optional away); /// Set the fan mode of the climate device. ClimateCall &set_fan_mode(ClimateFanMode fan_mode); /// Set the fan mode of the climate device. @@ -97,8 +93,6 @@ class ClimateCall { const optional &get_target_temperature() const; const optional &get_target_temperature_low() const; const optional &get_target_temperature_high() const; - ESPDEPRECATED("get_away() is deprecated, please use .get_preset() instead", "v1.20") - optional get_away() const; const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &get_custom_fan_mode() const; @@ -166,11 +160,6 @@ struct ClimateDeviceRestoreState { */ class Climate : public EntityBase { public: - /// Construct a climate device with empty name (will be set later). - Climate(); - /// Construct a climate device with a name. - Climate(const std::string &name); - /// The active mode of the climate device. ClimateMode mode{CLIMATE_MODE_OFF}; /// The active state of the climate device. @@ -189,14 +178,6 @@ class Climate : public EntityBase { }; }; - /** Whether the climate device is in away mode. - * - * Away allows climate devices to have two different target temperature configs: - * one for normal mode and one for away mode. - */ - ESPDEPRECATED("away is deprecated, use preset instead", "v1.20") - bool away{false}; - /// The active fan mode of the climate device. optional fan_mode; @@ -219,6 +200,14 @@ class Climate : public EntityBase { */ void add_on_state_callback(std::function &&callback); + /** + * Add a callback for the climate device configuration; each time the configuration parameters of a climate device + * is updated (using perform() of a ClimateCall), this callback will be called, before any on_state callback. + * + * @param callback The callback to call. + */ + void add_on_control_callback(std::function &&callback); + /** Make a climate device control call, this is used to control the climate device, see the ClimateCall description * for more info. * @return A new ClimateCall instance targeting this climate device. @@ -241,7 +230,7 @@ class Climate : public EntityBase { void set_visual_min_temperature_override(float visual_min_temperature_override); void set_visual_max_temperature_override(float visual_max_temperature_override); - void set_visual_temperature_step_override(float visual_temperature_step_override); + void set_visual_temperature_step_override(float target, float current); protected: friend ClimateCall; @@ -282,14 +271,15 @@ class Climate : public EntityBase { */ void save_state_(); - uint32_t hash_base() override; void dump_traits_(const char *tag); CallbackManager state_callback_{}; + CallbackManager control_callback_{}; ESPPreferenceObject rtc_; optional visual_min_temperature_override_{}; optional visual_max_temperature_override_{}; - optional visual_temperature_step_override_{}; + optional visual_target_temperature_step_override_{}; + optional visual_current_temperature_step_override_{}; }; } // namespace climate diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index e46159a750..794f45ccd6 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -62,6 +62,8 @@ const LogString *climate_fan_mode_to_string(ClimateFanMode fan_mode) { return LOG_STR("FOCUS"); case climate::CLIMATE_FAN_DIFFUSE: return LOG_STR("DIFFUSE"); + case climate::CLIMATE_FAN_QUIET: + return LOG_STR("QUIET"); default: return LOG_STR("UNKNOWN"); } diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index 3e5626919c..c5245812c7 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -62,6 +62,8 @@ enum ClimateFanMode : uint8_t { CLIMATE_FAN_FOCUS = 7, /// The fan mode is set to Diffuse CLIMATE_FAN_DIFFUSE = 8, + /// The fan mode is set to Quiet + CLIMATE_FAN_QUIET = 9, }; /// Enum for all modes a climate swing can be in @@ -76,7 +78,7 @@ enum ClimateSwingMode : uint8_t { CLIMATE_SWING_HORIZONTAL = 3, }; -/// Enum for all modes a climate swing can be in +/// Enum for all preset modes enum ClimatePreset : uint8_t { /// No preset is active CLIMATE_PRESET_NONE = 0, @@ -108,7 +110,7 @@ const LogString *climate_fan_mode_to_string(ClimateFanMode mode); /// Convert the given ClimateSwingMode to a human-readable string. const LogString *climate_swing_mode_to_string(ClimateSwingMode mode); -/// Convert the given ClimateSwingMode to a human-readable string. +/// Convert the given PresetMode to a human-readable string. const LogString *climate_preset_to_string(ClimatePreset preset); } // namespace climate diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp index 16c9cd05be..342dffaad6 100644 --- a/esphome/components/climate/climate_traits.cpp +++ b/esphome/components/climate/climate_traits.cpp @@ -1,19 +1,14 @@ #include "climate_traits.h" -#include namespace esphome { namespace climate { -int8_t ClimateTraits::get_temperature_accuracy_decimals() const { - // use printf %g to find number of digits based on temperature step - char buf[32]; - sprintf(buf, "%.5g", this->visual_temperature_step_); - std::string str{buf}; - size_t dot_pos = str.find('.'); - if (dot_pos == std::string::npos) - return 0; +int8_t ClimateTraits::get_target_temperature_accuracy_decimals() const { + return step_to_accuracy_decimals(this->visual_target_temperature_step_); +} - return str.length() - dot_pos - 1; +int8_t ClimateTraits::get_current_temperature_accuracy_decimals() const { + return step_to_accuracy_decimals(this->visual_current_temperature_step_); } } // namespace climate diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index d113510eeb..e8c2db6c06 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -28,7 +28,7 @@ namespace climate { * - supports action - if the climate device supports reporting the active * current action of the device with the action property. * - supports fan modes - optionally, if it has a fan which can be configured in different ways: - * - on, off, auto, high, medium, low, middle, focus, diffuse + * - on, off, auto, high, medium, low, middle, focus, diffuse, quiet * - supports swing modes - optionally, if it has a swing which can be configured in different ways: * - off, both, vertical, horizontal * @@ -117,15 +117,6 @@ class ClimateTraits { bool supports_custom_preset(const std::string &custom_preset) const { return supported_custom_presets_.count(custom_preset); } - ESPDEPRECATED("This method is deprecated, use set_supported_presets() instead", "v1.20") - void set_supports_away(bool supports) { - if (supports) { - supported_presets_.insert(CLIMATE_PRESET_AWAY); - supported_presets_.insert(CLIMATE_PRESET_HOME); - } - } - ESPDEPRECATED("This method is deprecated, use supports_preset() instead", "v1.20") - bool get_supports_away() const { return supports_preset(CLIMATE_PRESET_AWAY); } void set_supported_swing_modes(std::set modes) { supported_swing_modes_ = std::move(modes); } void add_supported_swing_mode(ClimateSwingMode mode) { supported_swing_modes_.insert(mode); } @@ -141,15 +132,26 @@ class ClimateTraits { } bool supports_swing_mode(ClimateSwingMode swing_mode) const { return supported_swing_modes_.count(swing_mode); } bool get_supports_swing_modes() const { return !supported_swing_modes_.empty(); } - std::set get_supported_swing_modes() { return supported_swing_modes_; } + std::set get_supported_swing_modes() const { return supported_swing_modes_; } float get_visual_min_temperature() const { return visual_min_temperature_; } void set_visual_min_temperature(float visual_min_temperature) { visual_min_temperature_ = visual_min_temperature; } float get_visual_max_temperature() const { return visual_max_temperature_; } void set_visual_max_temperature(float visual_max_temperature) { visual_max_temperature_ = visual_max_temperature; } - float get_visual_temperature_step() const { return visual_temperature_step_; } - int8_t get_temperature_accuracy_decimals() const; - void set_visual_temperature_step(float temperature_step) { visual_temperature_step_ = temperature_step; } + float get_visual_target_temperature_step() const { return visual_target_temperature_step_; } + float get_visual_current_temperature_step() const { return visual_current_temperature_step_; } + void set_visual_target_temperature_step(float temperature_step) { + visual_target_temperature_step_ = temperature_step; + } + void set_visual_current_temperature_step(float temperature_step) { + visual_current_temperature_step_ = temperature_step; + } + void set_visual_temperature_step(float temperature_step) { + visual_target_temperature_step_ = temperature_step; + visual_current_temperature_step_ = temperature_step; + } + int8_t get_target_temperature_accuracy_decimals() const; + int8_t get_current_temperature_accuracy_decimals() const; protected: void set_mode_support_(climate::ClimateMode mode, bool supported) { @@ -186,7 +188,8 @@ class ClimateTraits { float visual_min_temperature_{10}; float visual_max_temperature_{30}; - float visual_temperature_step_{0.1}; + float visual_target_temperature_step_{0.1}; + float visual_current_temperature_step_{0.1}; }; } // namespace climate diff --git a/esphome/components/color/__init__.py b/esphome/components/color/__init__.py index 47679fcc68..9a85eace75 100644 --- a/esphome/components/color/__init__.py +++ b/esphome/components/color/__init__.py @@ -10,23 +10,42 @@ CONF_RED_INT = "red_int" CONF_GREEN_INT = "green_int" CONF_BLUE_INT = "blue_int" CONF_WHITE_INT = "white_int" - -CONFIG_SCHEMA = cv.Schema( - { - cv.Required(CONF_ID): cv.declare_id(ColorStruct), - cv.Exclusive(CONF_RED, "red"): cv.percentage, - cv.Exclusive(CONF_RED_INT, "red"): cv.uint8_t, - cv.Exclusive(CONF_GREEN, "green"): cv.percentage, - cv.Exclusive(CONF_GREEN_INT, "green"): cv.uint8_t, - cv.Exclusive(CONF_BLUE, "blue"): cv.percentage, - cv.Exclusive(CONF_BLUE_INT, "blue"): cv.uint8_t, - cv.Exclusive(CONF_WHITE, "white"): cv.percentage, - cv.Exclusive(CONF_WHITE_INT, "white"): cv.uint8_t, - } -).extend(cv.COMPONENT_SCHEMA) +CONF_HEX = "hex" -async def to_code(config): +def hex_color(value): + if len(value) != 6: + raise cv.Invalid("Color must have six digits") + try: + return (int(value[0:2], 16), int(value[2:4], 16), int(value[4:6], 16)) + except ValueError as exc: + raise cv.Invalid("Color must be hexadecimal") from exc + + +CONFIG_SCHEMA = cv.Any( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(ColorStruct), + cv.Exclusive(CONF_RED, "red"): cv.percentage, + cv.Exclusive(CONF_RED_INT, "red"): cv.uint8_t, + cv.Exclusive(CONF_GREEN, "green"): cv.percentage, + cv.Exclusive(CONF_GREEN_INT, "green"): cv.uint8_t, + cv.Exclusive(CONF_BLUE, "blue"): cv.percentage, + cv.Exclusive(CONF_BLUE_INT, "blue"): cv.uint8_t, + cv.Exclusive(CONF_WHITE, "white"): cv.percentage, + cv.Exclusive(CONF_WHITE_INT, "white"): cv.uint8_t, + } + ).extend(cv.COMPONENT_SCHEMA), + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(ColorStruct), + cv.Required(CONF_HEX): hex_color, + } + ).extend(cv.COMPONENT_SCHEMA), +) + + +def from_rgbw(config): r = 0 if CONF_RED in config: r = int(config[CONF_RED] * 255) @@ -51,6 +70,16 @@ async def to_code(config): elif CONF_WHITE_INT in config: w = config[CONF_WHITE_INT] + return (r, g, b, w) + + +async def to_code(config): + if CONF_HEX in config: + r, g, b = config[CONF_HEX] + w = 0 + else: + r, g, b, w = from_rgbw(config) + cg.new_variable( config[CONF_ID], cg.StructInitializer(ColorStruct, ("r", r), ("g", g), ("b", b), ("w", w)), diff --git a/esphome/components/copy/__init__.py b/esphome/components/copy/__init__.py new file mode 100644 index 0000000000..7594894650 --- /dev/null +++ b/esphome/components/copy/__init__.py @@ -0,0 +1,5 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@OttoWinter"] + +copy_ns = cg.esphome_ns.namespace("copy") diff --git a/esphome/components/copy/binary_sensor/__init__.py b/esphome/components/copy/binary_sensor/__init__.py new file mode 100644 index 0000000000..1b6836fae7 --- /dev/null +++ b/esphome/components/copy/binary_sensor/__init__.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_SOURCE_ID, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopyBinarySensor = copy_ns.class_( + "CopyBinarySensor", binary_sensor.BinarySensor, cg.Component +) + + +CONFIG_SCHEMA = ( + binary_sensor.binary_sensor_schema(CopyBinarySensor) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(binary_sensor.BinarySensor), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), + inherit_property_from(CONF_DEVICE_CLASS, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/binary_sensor/copy_binary_sensor.cpp b/esphome/components/copy/binary_sensor/copy_binary_sensor.cpp new file mode 100644 index 0000000000..0d96f58750 --- /dev/null +++ b/esphome/components/copy/binary_sensor/copy_binary_sensor.cpp @@ -0,0 +1,18 @@ +#include "copy_binary_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.binary_sensor"; + +void CopyBinarySensor::setup() { + source_->add_on_state_callback([this](bool value) { this->publish_state(value); }); + if (source_->has_state()) + this->publish_state(source_->state); +} + +void CopyBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Copy Binary Sensor", this); } + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/binary_sensor/copy_binary_sensor.h b/esphome/components/copy/binary_sensor/copy_binary_sensor.h new file mode 100644 index 0000000000..d62ed13c76 --- /dev/null +++ b/esphome/components/copy/binary_sensor/copy_binary_sensor.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace copy { + +class CopyBinarySensor : public binary_sensor::BinarySensor, public Component { + public: + void set_source(binary_sensor::BinarySensor *source) { source_ = source; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + binary_sensor::BinarySensor *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/button/__init__.py b/esphome/components/copy/button/__init__.py new file mode 100644 index 0000000000..626a5a8db1 --- /dev/null +++ b/esphome/components/copy/button/__init__.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import button +from esphome.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_ID, + CONF_SOURCE_ID, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopyButton = copy_ns.class_("CopyButton", button.Button, cg.Component) + + +CONFIG_SCHEMA = ( + button.button_schema(CopyButton) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(button.Button), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), + inherit_property_from(CONF_DEVICE_CLASS, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await button.register_button(var, config) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/button/copy_button.cpp b/esphome/components/copy/button/copy_button.cpp new file mode 100644 index 0000000000..595388775c --- /dev/null +++ b/esphome/components/copy/button/copy_button.cpp @@ -0,0 +1,14 @@ +#include "copy_button.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.button"; + +void CopyButton::dump_config() { LOG_BUTTON("", "Copy Button", this); } + +void CopyButton::press_action() { source_->press(); } + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/button/copy_button.h b/esphome/components/copy/button/copy_button.h new file mode 100644 index 0000000000..9996ca0c65 --- /dev/null +++ b/esphome/components/copy/button/copy_button.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/button/button.h" + +namespace esphome { +namespace copy { + +class CopyButton : public button::Button, public Component { + public: + void set_source(button::Button *source) { source_ = source; } + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void press_action() override; + + button::Button *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/cover/__init__.py b/esphome/components/copy/cover/__init__.py new file mode 100644 index 0000000000..155e22883b --- /dev/null +++ b/esphome/components/copy/cover/__init__.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import cover +from esphome.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_ID, + CONF_SOURCE_ID, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopyCover = copy_ns.class_("CopyCover", cover.Cover, cg.Component) + + +CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(CopyCover), + cv.Required(CONF_SOURCE_ID): cv.use_id(cover.Cover), + } +).extend(cv.COMPONENT_SCHEMA) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), + inherit_property_from(CONF_DEVICE_CLASS, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cover.register_cover(var, config) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/cover/copy_cover.cpp b/esphome/components/copy/cover/copy_cover.cpp new file mode 100644 index 0000000000..28f8c9877c --- /dev/null +++ b/esphome/components/copy/cover/copy_cover.cpp @@ -0,0 +1,51 @@ +#include "copy_cover.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.cover"; + +void CopyCover::setup() { + source_->add_on_state_callback([this]() { + this->current_operation = this->source_->current_operation; + this->position = this->source_->position; + this->tilt = this->source_->tilt; + this->publish_state(); + }); + + this->current_operation = this->source_->current_operation; + this->position = this->source_->position; + this->tilt = this->source_->tilt; + this->publish_state(); +} + +void CopyCover::dump_config() { LOG_COVER("", "Copy Cover", this); } + +cover::CoverTraits CopyCover::get_traits() { + auto base = source_->get_traits(); + cover::CoverTraits traits{}; + // copy traits manually so it doesn't break when new options are added + // but the control() method hasn't implemented them yet. + traits.set_is_assumed_state(base.get_is_assumed_state()); + traits.set_supports_stop(base.get_supports_stop()); + traits.set_supports_position(base.get_supports_position()); + traits.set_supports_tilt(base.get_supports_tilt()); + traits.set_supports_toggle(base.get_supports_toggle()); + return traits; +} + +void CopyCover::control(const cover::CoverCall &call) { + auto call2 = source_->make_call(); + call2.set_stop(call.get_stop()); + if (call.get_tilt().has_value()) + call2.set_tilt(*call.get_tilt()); + if (call.get_position().has_value()) + call2.set_position(*call.get_position()); + if (call.get_tilt().has_value()) + call2.set_tilt(*call.get_tilt()); + call2.perform(); +} + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/cover/copy_cover.h b/esphome/components/copy/cover/copy_cover.h new file mode 100644 index 0000000000..fb278523ff --- /dev/null +++ b/esphome/components/copy/cover/copy_cover.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/cover/cover.h" + +namespace esphome { +namespace copy { + +class CopyCover : public cover::Cover, public Component { + public: + void set_source(cover::Cover *source) { source_ = source; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + cover::CoverTraits get_traits() override; + + protected: + void control(const cover::CoverCall &call) override; + + cover::Cover *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/fan/__init__.py b/esphome/components/copy/fan/__init__.py new file mode 100644 index 0000000000..22672c02d8 --- /dev/null +++ b/esphome/components/copy/fan/__init__.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import fan +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_ID, + CONF_SOURCE_ID, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopyFan = copy_ns.class_("CopyFan", fan.Fan, cg.Component) + + +CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(CopyFan), + cv.Required(CONF_SOURCE_ID): cv.use_id(fan.Fan), + } +).extend(cv.COMPONENT_SCHEMA) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await fan.register_fan(var, config) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp new file mode 100644 index 0000000000..74d9da279f --- /dev/null +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -0,0 +1,53 @@ +#include "copy_fan.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.fan"; + +void CopyFan::setup() { + source_->add_on_state_callback([this]() { + this->state = source_->state; + this->oscillating = source_->oscillating; + this->speed = source_->speed; + this->direction = source_->direction; + this->publish_state(); + }); + + this->state = source_->state; + this->oscillating = source_->oscillating; + this->speed = source_->speed; + this->direction = source_->direction; + this->publish_state(); +} + +void CopyFan::dump_config() { LOG_FAN("", "Copy Fan", this); } + +fan::FanTraits CopyFan::get_traits() { + fan::FanTraits traits; + auto base = source_->get_traits(); + // copy traits manually so it doesn't break when new options are added + // but the control() method hasn't implemented them yet. + traits.set_oscillation(base.supports_oscillation()); + traits.set_speed(base.supports_speed()); + traits.set_supported_speed_count(base.supported_speed_count()); + traits.set_direction(base.supports_direction()); + return traits; +} + +void CopyFan::control(const fan::FanCall &call) { + auto call2 = source_->make_call(); + if (call.get_state().has_value()) + call2.set_state(*call.get_state()); + if (call.get_oscillating().has_value()) + call2.set_oscillating(*call.get_oscillating()); + if (call.get_speed().has_value()) + call2.set_speed(*call.get_speed()); + if (call.get_direction().has_value()) + call2.set_direction(*call.get_direction()); + call2.perform(); +} + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/fan/copy_fan.h b/esphome/components/copy/fan/copy_fan.h new file mode 100644 index 0000000000..1a69810510 --- /dev/null +++ b/esphome/components/copy/fan/copy_fan.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/fan/fan.h" + +namespace esphome { +namespace copy { + +class CopyFan : public fan::Fan, public Component { + public: + void set_source(fan::Fan *source) { source_ = source; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + fan::FanTraits get_traits() override; + + protected: + void control(const fan::FanCall &call) override; + ; + + fan::Fan *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/lock/__init__.py b/esphome/components/copy/lock/__init__.py new file mode 100644 index 0000000000..d19e4a5807 --- /dev/null +++ b/esphome/components/copy/lock/__init__.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import lock +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_ID, + CONF_SOURCE_ID, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopyLock = copy_ns.class_("CopyLock", lock.Lock, cg.Component) + + +CONFIG_SCHEMA = lock.LOCK_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(CopyLock), + cv.Required(CONF_SOURCE_ID): cv.use_id(lock.Lock), + } +).extend(cv.COMPONENT_SCHEMA) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await lock.register_lock(var, config) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/lock/copy_lock.cpp b/esphome/components/copy/lock/copy_lock.cpp new file mode 100644 index 0000000000..67a8acffec --- /dev/null +++ b/esphome/components/copy/lock/copy_lock.cpp @@ -0,0 +1,29 @@ +#include "copy_lock.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.lock"; + +void CopyLock::setup() { + source_->add_on_state_callback([this]() { this->publish_state(source_->state); }); + + traits.set_assumed_state(source_->traits.get_assumed_state()); + traits.set_requires_code(source_->traits.get_requires_code()); + traits.set_supported_states(source_->traits.get_supported_states()); + traits.set_supports_open(source_->traits.get_supports_open()); + + this->publish_state(source_->state); +} + +void CopyLock::dump_config() { LOG_LOCK("", "Copy Lock", this); } + +void CopyLock::control(const lock::LockCall &call) { + auto call2 = source_->make_call(); + call2.set_state(call.get_state()); + call2.perform(); +} + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/lock/copy_lock.h b/esphome/components/copy/lock/copy_lock.h new file mode 100644 index 0000000000..0554013674 --- /dev/null +++ b/esphome/components/copy/lock/copy_lock.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/lock/lock.h" + +namespace esphome { +namespace copy { + +class CopyLock : public lock::Lock, public Component { + public: + void set_source(lock::Lock *source) { source_ = source; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void control(const lock::LockCall &call) override; + + lock::Lock *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/number/__init__.py b/esphome/components/copy/number/__init__.py new file mode 100644 index 0000000000..204518da39 --- /dev/null +++ b/esphome/components/copy/number/__init__.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import number +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_MODE, + CONF_SOURCE_ID, + CONF_UNIT_OF_MEASUREMENT, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopyNumber = copy_ns.class_("CopyNumber", number.Number, cg.Component) + + +CONFIG_SCHEMA = ( + number.number_schema(CopyNumber) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(number.Number), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), + inherit_property_from(CONF_UNIT_OF_MEASUREMENT, CONF_SOURCE_ID), + inherit_property_from(CONF_MODE, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = await number.new_number(config, min_value=0, max_value=0, step=0) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/number/copy_number.cpp b/esphome/components/copy/number/copy_number.cpp new file mode 100644 index 0000000000..46dc200b73 --- /dev/null +++ b/esphome/components/copy/number/copy_number.cpp @@ -0,0 +1,29 @@ +#include "copy_number.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.number"; + +void CopyNumber::setup() { + source_->add_on_state_callback([this](float value) { this->publish_state(value); }); + + traits.set_min_value(source_->traits.get_min_value()); + traits.set_max_value(source_->traits.get_max_value()); + traits.set_step(source_->traits.get_step()); + + if (source_->has_state()) + this->publish_state(source_->state); +} + +void CopyNumber::dump_config() { LOG_NUMBER("", "Copy Number", this); } + +void CopyNumber::control(float value) { + auto call2 = source_->make_call(); + call2.set_value(value); + call2.perform(); +} + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/number/copy_number.h b/esphome/components/copy/number/copy_number.h new file mode 100644 index 0000000000..1ad956fec4 --- /dev/null +++ b/esphome/components/copy/number/copy_number.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/number/number.h" + +namespace esphome { +namespace copy { + +class CopyNumber : public number::Number, public Component { + public: + void set_source(number::Number *source) { source_ = source; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void control(float value) override; + + number::Number *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/select/__init__.py b/esphome/components/copy/select/__init__.py new file mode 100644 index 0000000000..6254ed03e1 --- /dev/null +++ b/esphome/components/copy/select/__init__.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import select +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_ID, + CONF_SOURCE_ID, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopySelect = copy_ns.class_("CopySelect", select.Select, cg.Component) + + +CONFIG_SCHEMA = ( + select.select_schema(CopySelect) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(select.Select), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await select.register_select(var, config, options=[]) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp new file mode 100644 index 0000000000..bdcbd0b42c --- /dev/null +++ b/esphome/components/copy/select/copy_select.cpp @@ -0,0 +1,27 @@ +#include "copy_select.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.select"; + +void CopySelect::setup() { + source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); }); + + traits.set_options(source_->traits.get_options()); + + if (source_->has_state()) + this->publish_state(source_->state); +} + +void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); } + +void CopySelect::control(const std::string &value) { + auto call = source_->make_call(); + call.set_option(value); + call.perform(); +} + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/select/copy_select.h b/esphome/components/copy/select/copy_select.h new file mode 100644 index 0000000000..c8666cd394 --- /dev/null +++ b/esphome/components/copy/select/copy_select.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/select/select.h" + +namespace esphome { +namespace copy { + +class CopySelect : public select::Select, public Component { + public: + void set_source(select::Select *source) { source_ = source; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void control(const std::string &value) override; + + select::Select *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/sensor/__init__.py b/esphome/components/copy/sensor/__init__.py new file mode 100644 index 0000000000..8e78cda7c7 --- /dev/null +++ b/esphome/components/copy/sensor/__init__.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_SOURCE_ID, + CONF_STATE_CLASS, + CONF_UNIT_OF_MEASUREMENT, + CONF_ACCURACY_DECIMALS, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopySensor = copy_ns.class_("CopySensor", sensor.Sensor, cg.Component) + + +CONFIG_SCHEMA = ( + sensor.sensor_schema(CopySensor) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(sensor.Sensor), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_UNIT_OF_MEASUREMENT, CONF_SOURCE_ID), + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ACCURACY_DECIMALS, CONF_SOURCE_ID), + inherit_property_from(CONF_DEVICE_CLASS, CONF_SOURCE_ID), + inherit_property_from(CONF_STATE_CLASS, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/sensor/copy_sensor.cpp b/esphome/components/copy/sensor/copy_sensor.cpp new file mode 100644 index 0000000000..a47a0cf22b --- /dev/null +++ b/esphome/components/copy/sensor/copy_sensor.cpp @@ -0,0 +1,18 @@ +#include "copy_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.sensor"; + +void CopySensor::setup() { + source_->add_on_state_callback([this](float value) { this->publish_state(value); }); + if (source_->has_state()) + this->publish_state(source_->state); +} + +void CopySensor::dump_config() { LOG_SENSOR("", "Copy Sensor", this); } + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/sensor/copy_sensor.h b/esphome/components/copy/sensor/copy_sensor.h new file mode 100644 index 0000000000..1ae790ada3 --- /dev/null +++ b/esphome/components/copy/sensor/copy_sensor.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace copy { + +class CopySensor : public sensor::Sensor, public Component { + public: + void set_source(sensor::Sensor *source) { source_ = source; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + sensor::Sensor *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/switch/__init__.py b/esphome/components/copy/switch/__init__.py new file mode 100644 index 0000000000..beffbe7fbb --- /dev/null +++ b/esphome/components/copy/switch/__init__.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch +from esphome.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_SOURCE_ID, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopySwitch = copy_ns.class_("CopySwitch", switch.Switch, cg.Component) + + +CONFIG_SCHEMA = ( + switch.switch_schema(CopySwitch) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(switch.Switch), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), + inherit_property_from(CONF_DEVICE_CLASS, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = await switch.new_switch(config) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/switch/copy_switch.cpp b/esphome/components/copy/switch/copy_switch.cpp new file mode 100644 index 0000000000..8a9fbb03dd --- /dev/null +++ b/esphome/components/copy/switch/copy_switch.cpp @@ -0,0 +1,26 @@ +#include "copy_switch.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.switch"; + +void CopySwitch::setup() { + source_->add_on_state_callback([this](float value) { this->publish_state(value); }); + + this->publish_state(source_->state); +} + +void CopySwitch::dump_config() { LOG_SWITCH("", "Copy Switch", this); } + +void CopySwitch::write_state(bool state) { + if (state) { + source_->turn_on(); + } else { + source_->turn_off(); + } +} + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/switch/copy_switch.h b/esphome/components/copy/switch/copy_switch.h new file mode 100644 index 0000000000..26cb254ab3 --- /dev/null +++ b/esphome/components/copy/switch/copy_switch.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace copy { + +class CopySwitch : public switch_::Switch, public Component { + public: + void set_source(switch_::Switch *source) { source_ = source; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void write_state(bool state) override; + + switch_::Switch *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/text_sensor/__init__.py b/esphome/components/copy/text_sensor/__init__.py new file mode 100644 index 0000000000..5b59f21319 --- /dev/null +++ b/esphome/components/copy/text_sensor/__init__.py @@ -0,0 +1,37 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_SOURCE_ID, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopyTextSensor = copy_ns.class_("CopyTextSensor", text_sensor.TextSensor, cg.Component) + + +CONFIG_SCHEMA = ( + text_sensor.text_sensor_schema(CopyTextSensor) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(text_sensor.TextSensor), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = await text_sensor.new_text_sensor(config) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/text_sensor/copy_text_sensor.cpp b/esphome/components/copy/text_sensor/copy_text_sensor.cpp new file mode 100644 index 0000000000..95fa6d7a1b --- /dev/null +++ b/esphome/components/copy/text_sensor/copy_text_sensor.cpp @@ -0,0 +1,18 @@ +#include "copy_text_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.text_sensor"; + +void CopyTextSensor::setup() { + source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); }); + if (source_->has_state()) + this->publish_state(source_->state); +} + +void CopyTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Copy Sensor", this); } + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/text_sensor/copy_text_sensor.h b/esphome/components/copy/text_sensor/copy_text_sensor.h new file mode 100644 index 0000000000..fe91fe948b --- /dev/null +++ b/esphome/components/copy/text_sensor/copy_text_sensor.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome { +namespace copy { + +class CopyTextSensor : public text_sensor::TextSensor, public Component { + public: + void set_source(text_sensor::TextSensor *source) { source_ = source; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + text_sensor::TextSensor *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 0fd27f3f27..90e5ee1f03 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_ID, CONF_DEVICE_CLASS, CONF_STATE, + CONF_ON_OPEN, CONF_POSITION, CONF_POSITION_COMMAND_TOPIC, CONF_POSITION_STATE_TOPIC, @@ -16,6 +17,17 @@ from esphome.const import ( CONF_STOP, CONF_MQTT_ID, CONF_TRIGGER_ID, + DEVICE_CLASS_AWNING, + DEVICE_CLASS_BLIND, + DEVICE_CLASS_CURTAIN, + DEVICE_CLASS_DAMPER, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_GARAGE, + DEVICE_CLASS_GATE, + DEVICE_CLASS_SHADE, + DEVICE_CLASS_SHUTTER, + DEVICE_CLASS_WINDOW, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_helpers import setup_entity @@ -24,17 +36,17 @@ IS_PLATFORM_COMPONENT = True CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ - "", - "awning", - "blind", - "curtain", - "damper", - "door", - "garage", - "gate", - "shade", - "shutter", - "window", + DEVICE_CLASS_AWNING, + DEVICE_CLASS_BLIND, + DEVICE_CLASS_CURTAIN, + DEVICE_CLASS_DAMPER, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_GARAGE, + DEVICE_CLASS_GATE, + DEVICE_CLASS_SHADE, + DEVICE_CLASS_SHUTTER, + DEVICE_CLASS_WINDOW, ] cover_ns = cg.esphome_ns.namespace("cover") @@ -74,7 +86,6 @@ CoverClosedTrigger = cover_ns.class_( "CoverClosedTrigger", automation.Trigger.template() ) -CONF_ON_OPEN = "on_open" CONF_ON_CLOSED = "on_closed" COVER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 21f35f14de..d139bab8ee 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -31,9 +31,7 @@ const char *cover_operation_to_str(CoverOperation op) { } } -Cover::Cover(const std::string &name) : EntityBase(name), position{COVER_OPEN} {} - -uint32_t Cover::hash_base() { return 1727367479UL; } +Cover::Cover() : position{COVER_OPEN} {} CoverCall::CoverCall(Cover *parent) : parent_(parent) {} CoverCall &CoverCall::set_command(const char *command) { @@ -147,7 +145,7 @@ CoverCall &CoverCall::set_stop(bool stop) { return *this; } bool CoverCall::get_stop() const { return this->stop_; } -void Cover::set_device_class(const std::string &device_class) { this->device_class_override_ = device_class; } + CoverCall Cover::make_call() { return {this}; } void Cover::open() { auto call = this->make_call(); @@ -206,18 +204,9 @@ optional Cover::restore_state_() { return {}; return recovered; } -Cover::Cover() : Cover("") {} -std::string Cover::get_device_class() { - if (this->device_class_override_.has_value()) - return *this->device_class_override_; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - return this->device_class(); -#pragma GCC diagnostic pop -} + bool Cover::is_fully_open() const { return this->position == COVER_OPEN; } bool Cover::is_fully_closed() const { return this->position == COVER_CLOSED; } -std::string Cover::device_class() { return ""; } CoverCall CoverRestoreState::to_call(Cover *cover) { auto call = cover->make_call(); diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 1b5d3a8fa1..89598a9636 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -108,10 +108,9 @@ const char *cover_operation_to_str(CoverOperation op); * to control all values of the cover. Also implement get_traits() to return what operations * the cover supports. */ -class Cover : public EntityBase { +class Cover : public EntityBase, public EntityBase_DeviceClass { public: explicit Cover(); - explicit Cover(const std::string &name); /// The current operation of the cover (idle, opening, closing). CoverOperation current_operation{COVER_OPERATION_IDLE}; @@ -141,8 +140,9 @@ class Cover : public EntityBase { /** Stop the cover. * * This is a legacy method and may be removed later, please use `.make_call()` instead. + * As per solution from issue #2885 the call should include perform() */ - ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop() instead.", "2021.9") + ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop().perform() instead.", "2021.9") void stop(); void add_on_state_callback(std::function &&f); @@ -157,8 +157,6 @@ class Cover : public EntityBase { void publish_state(bool save = true); virtual CoverTraits get_traits() = 0; - void set_device_class(const std::string &device_class); - std::string get_device_class(); /// Helper method to check if the cover is fully open. Equivalent to comparing .position against 1.0 bool is_fully_open() const; @@ -170,17 +168,9 @@ class Cover : public EntityBase { virtual void control(const CoverCall &call) = 0; - /** Override this to set the default device class. - * - * @deprecated This method is deprecated, set the property during config validation instead. (2022.1) - */ - virtual std::string device_class(); - optional restore_state_(); - uint32_t hash_base() override; CallbackManager state_callback_{}; - optional device_class_override_{}; ESPPreferenceObject rtc_; }; diff --git a/esphome/components/cover/cover_traits.h b/esphome/components/cover/cover_traits.h index fb30883f77..79001c3b03 100644 --- a/esphome/components/cover/cover_traits.h +++ b/esphome/components/cover/cover_traits.h @@ -15,12 +15,15 @@ class CoverTraits { void set_supports_tilt(bool supports_tilt) { this->supports_tilt_ = supports_tilt; } bool get_supports_toggle() const { return this->supports_toggle_; } void set_supports_toggle(bool supports_toggle) { this->supports_toggle_ = supports_toggle; } + bool get_supports_stop() const { return this->supports_stop_; } + void set_supports_stop(bool supports_stop) { this->supports_stop_ = supports_stop; } protected: bool is_assumed_state_{false}; bool supports_position_{false}; bool supports_tilt_{false}; bool supports_toggle_{false}; + bool supports_stop_{false}; }; } // namespace cover diff --git a/esphome/components/cs5460a/cs5460a.cpp b/esphome/components/cs5460a/cs5460a.cpp index b0c0531936..fb8e50b87a 100644 --- a/esphome/components/cs5460a/cs5460a.cpp +++ b/esphome/components/cs5460a/cs5460a.cpp @@ -305,7 +305,7 @@ bool CS5460AComponent::check_status_() { voltage_sensor_->publish_state(raw_voltage * voltage_multiplier_); if (power_sensor_ != nullptr && raw_energy != prev_raw_energy_) { - int32_t raw = (int32_t)(raw_energy << 8) >> 8; /* Sign-extend */ + int32_t raw = (int32_t) (raw_energy << 8) >> 8; /* Sign-extend */ power_sensor_->publish_state(raw * power_multiplier_); prev_raw_energy_ = raw_energy; } diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 25d75da3e6..f232f35ea6 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -13,8 +13,9 @@ void CSE7766Component::loop() { this->raw_data_index_ = 0; } - if (this->available() == 0) + if (this->available() == 0) { return; + } this->last_transmission_ = now; while (this->available() != 0) { @@ -22,6 +23,7 @@ void CSE7766Component::loop() { if (!this->check_byte_()) { this->raw_data_index_ = 0; this->status_set_warning(); + continue; } if (this->raw_data_index_ == 23) { @@ -51,8 +53,9 @@ bool CSE7766Component::check_byte_() { if (index == 23) { uint8_t checksum = 0; - for (uint8_t i = 2; i < 23; i++) + for (uint8_t i = 2; i < 23; i++) { checksum += this->raw_data_[i]; + } if (checksum != this->raw_data_[23]) { ESP_LOGW(TAG, "Invalid checksum from CSE7766: 0x%02X != 0x%02X", checksum, this->raw_data_[23]); @@ -66,20 +69,34 @@ bool CSE7766Component::check_byte_() { void CSE7766Component::parse_data_() { ESP_LOGVV(TAG, "CSE7766 Data: "); for (uint8_t i = 0; i < 23; i++) { - ESP_LOGVV(TAG, " i=%u: 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", i, BYTE_TO_BINARY(this->raw_data_[i]), + ESP_LOGVV(TAG, " %u: 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", i + 1, BYTE_TO_BINARY(this->raw_data_[i]), this->raw_data_[i]); } uint8_t header1 = this->raw_data_[0]; if (header1 == 0xAA) { - ESP_LOGW(TAG, "CSE7766 not calibrated!"); + ESP_LOGE(TAG, "CSE7766 not calibrated!"); return; } - if ((header1 & 0xF0) == 0xF0 && ((header1 >> 0) & 1) == 1) { - ESP_LOGW(TAG, "CSE7766 reports abnormal hardware: (0x%02X)", header1); - ESP_LOGW(TAG, " Coefficient storage area is abnormal."); - return; + bool power_cycle_exceeds_range = false; + + if ((header1 & 0xF0) == 0xF0) { + if (header1 & 0xD) { + ESP_LOGE(TAG, "CSE7766 reports abnormal external circuit or chip damage: (0x%02X)", header1); + if (header1 & (1 << 3)) { + ESP_LOGE(TAG, " Voltage cycle exceeds range."); + } + if (header1 & (1 << 2)) { + ESP_LOGE(TAG, " Current cycle exceeds range."); + } + if (header1 & (1 << 0)) { + ESP_LOGE(TAG, " Coefficient storage area is abnormal."); + } + return; + } + + power_cycle_exceeds_range = header1 & (1 << 1); } uint32_t voltage_calib = this->get_24_bit_uint_(2); @@ -92,46 +109,29 @@ void CSE7766Component::parse_data_() { uint8_t adj = this->raw_data_[20]; uint32_t cf_pulses = (this->raw_data_[21] << 8) + this->raw_data_[22]; - bool power_ok = true; - bool voltage_ok = true; - bool current_ok = true; - - if (header1 > 0xF0) { - // ESP_LOGV(TAG, "CSE7766 reports abnormal hardware: (0x%02X)", byte); - if ((header1 >> 3) & 1) { - ESP_LOGV(TAG, " Voltage cycle exceeds range."); - voltage_ok = false; - } - if ((header1 >> 2) & 1) { - ESP_LOGV(TAG, " Current cycle exceeds range."); - current_ok = false; - } - if ((header1 >> 1) & 1) { - ESP_LOGV(TAG, " Power cycle exceeds range."); - power_ok = false; - } - if ((header1 >> 0) & 1) { - ESP_LOGV(TAG, " Coefficient storage area is abnormal."); - return; - } - } - - if ((adj & 0x40) == 0x40 && voltage_ok && current_ok) { + bool have_voltage = adj & 0x40; + if (have_voltage) { // voltage cycle of serial port outputted is a complete cycle; this->voltage_acc_ += voltage_calib / float(voltage_cycle); this->voltage_counts_ += 1; } - float power = 0; - if ((adj & 0x10) == 0x10 && voltage_ok && current_ok && power_ok) { + bool have_power = adj & 0x10; + float power = 0.0f; + + if (have_power) { // power cycle of serial port outputted is a complete cycle; - power = power_calib / float(power_cycle); + // According to the user manual, power cycle exceeding range means the measured power is 0 + if (!power_cycle_exceeds_range) { + power = power_calib / float(power_cycle); + } this->power_acc_ += power; this->power_counts_ += 1; uint32_t difference; - if (this->cf_pulses_last_ == 0) + if (this->cf_pulses_last_ == 0) { this->cf_pulses_last_ = cf_pulses; + } if (cf_pulses < this->cf_pulses_last_) { difference = cf_pulses + (0x10000 - this->cf_pulses_last_); @@ -139,41 +139,52 @@ void CSE7766Component::parse_data_() { difference = cf_pulses - this->cf_pulses_last_; } this->cf_pulses_last_ = cf_pulses; - this->energy_total_ += difference * float(power_calib) / 1000000.0 / 3600.0; + this->energy_total_ += difference * float(power_calib) / 1000000.0f / 3600.0f; + this->energy_total_counts_ += 1; } - if ((adj & 0x20) == 0x20 && current_ok && voltage_ok && power != 0.0) { + if (adj & 0x20) { // indicates current cycle of serial port outputted is a complete cycle; - this->current_acc_ += current_calib / float(current_cycle); + float current = 0.0f; + if (have_voltage && !have_power) { + // Testing has shown that when we have voltage and current but not power, that means the power is 0. + // We report a power of 0, which in turn means we should report a current of 0. + this->power_counts_ += 1; + } else if (power != 0.0f) { + current = current_calib / float(current_cycle); + } + this->current_acc_ += current; this->current_counts_ += 1; } } void CSE7766Component::update() { - float voltage = this->voltage_counts_ > 0 ? this->voltage_acc_ / this->voltage_counts_ : 0.0f; - float current = this->current_counts_ > 0 ? this->current_acc_ / this->current_counts_ : 0.0f; - float power = this->power_counts_ > 0 ? this->power_acc_ / this->power_counts_ : 0.0f; + const auto publish_state = [](const char *name, sensor::Sensor *sensor, float &acc, uint32_t &counts) { + if (counts != 0) { + const auto avg = acc / counts; - ESP_LOGV(TAG, "Got voltage_acc=%.2f current_acc=%.2f power_acc=%.2f", this->voltage_acc_, this->current_acc_, - this->power_acc_); - ESP_LOGV(TAG, "Got voltage_counts=%d current_counts=%d power_counts=%d", this->voltage_counts_, this->current_counts_, - this->power_counts_); - ESP_LOGD(TAG, "Got voltage=%.1fV current=%.1fA power=%.1fW", voltage, current, power); + ESP_LOGV(TAG, "Got %s_acc=%.2f %s_counts=%d %s=%.1f", name, acc, name, counts, name, avg); - if (this->voltage_sensor_ != nullptr) - this->voltage_sensor_->publish_state(voltage); - if (this->current_sensor_ != nullptr) - this->current_sensor_->publish_state(current); - if (this->power_sensor_ != nullptr) - this->power_sensor_->publish_state(power); - if (this->energy_sensor_ != nullptr) - this->energy_sensor_->publish_state(this->energy_total_); + if (sensor != nullptr) { + sensor->publish_state(avg); + } - this->voltage_acc_ = 0.0f; - this->current_acc_ = 0.0f; - this->power_acc_ = 0.0f; - this->voltage_counts_ = 0; - this->power_counts_ = 0; - this->current_counts_ = 0; + acc = 0.0f; + counts = 0; + } + }; + + publish_state("voltage", this->voltage_sensor_, this->voltage_acc_, this->voltage_counts_); + publish_state("current", this->current_sensor_, this->current_acc_, this->current_counts_); + publish_state("power", this->power_sensor_, this->power_acc_, this->power_counts_); + + if (this->energy_total_counts_ != 0) { + ESP_LOGV(TAG, "Got energy_total=%.2f energy_total_counts=%d", this->energy_total_, this->energy_total_counts_); + + if (this->energy_sensor_ != nullptr) { + this->energy_sensor_->publish_state(this->energy_total_); + } + this->energy_total_counts_ = 0; + } } uint32_t CSE7766Component::get_24_bit_uint_(uint8_t start_index) { diff --git a/esphome/components/cse7766/cse7766.h b/esphome/components/cse7766/cse7766.h index d6062c251c..2f30eec09f 100644 --- a/esphome/components/cse7766/cse7766.h +++ b/esphome/components/cse7766/cse7766.h @@ -39,6 +39,8 @@ class CSE7766Component : public PollingComponent, public uart::UARTDevice { uint32_t voltage_counts_{0}; uint32_t current_counts_{0}; uint32_t power_counts_{0}; + // Setting this to 1 means it will always publish 0 once at startup + uint32_t energy_total_counts_{1}; }; } // namespace cse7766 diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.cpp b/esphome/components/ct_clamp/ct_clamp_sensor.cpp index 51b0f1318c..d555befcde 100644 --- a/esphome/components/ct_clamp/ct_clamp_sensor.cpp +++ b/esphome/components/ct_clamp/ct_clamp_sensor.cpp @@ -33,7 +33,10 @@ void CTClampSensor::update() { const float rms_ac_dc_squared = this->sample_squared_sum_ / this->num_samples_; const float rms_dc = this->sample_sum_ / this->num_samples_; - const float rms_ac = std::sqrt(rms_ac_dc_squared - rms_dc * rms_dc); + const float rms_ac_squared = rms_ac_dc_squared - rms_dc * rms_dc; + float rms_ac = 0; + if (rms_ac_squared > 0) + rms_ac = std::sqrt(rms_ac_squared); ESP_LOGD(TAG, "'%s' - Raw AC Value: %.3fA after %d different samples (%d SPS)", this->name_.c_str(), rms_ac, this->num_samples_, 1000 * this->num_samples_ / this->sample_duration_); this->publish_state(rms_ac); diff --git a/esphome/components/ct_clamp/sensor.py b/esphome/components/ct_clamp/sensor.py index 049905d0a7..18ea5877d2 100644 --- a/esphome/components/ct_clamp/sensor.py +++ b/esphome/components/ct_clamp/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor, voltage_sampler from esphome.const import ( CONF_SENSOR, - CONF_ID, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT, UNIT_AMPERE, @@ -19,6 +18,7 @@ CTClampSensor = ct_clamp_ns.class_("CTClampSensor", sensor.Sensor, cg.PollingCom CONFIG_SCHEMA = ( sensor.sensor_schema( + CTClampSensor, unit_of_measurement=UNIT_AMPERE, accuracy_decimals=2, device_class=DEVICE_CLASS_CURRENT, @@ -26,7 +26,6 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.GenerateID(): cv.declare_id(CTClampSensor), cv.Required(CONF_SENSOR): cv.use_id(voltage_sampler.VoltageSampler), cv.Optional( CONF_SAMPLE_DURATION, default="200ms" @@ -38,9 +37,8 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_source(sens)) diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index 9f0a59377d..ff5ad43997 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -12,7 +12,9 @@ using namespace esphome::cover; CoverTraits CurrentBasedCover::get_traits() { auto traits = CoverTraits(); + traits.set_supports_stop(true); traits.set_supports_position(true); + traits.set_supports_toggle(true); traits.set_is_assumed_state(false); return traits; } @@ -20,6 +22,20 @@ void CurrentBasedCover::control(const CoverCall &call) { if (call.get_stop()) { this->direction_idle_(); } + if (call.get_toggle().has_value()) { + if (this->current_operation != COVER_OPERATION_IDLE) { + this->start_direction_(COVER_OPERATION_IDLE); + this->publish_state(); + } else { + if (this->position == COVER_CLOSED || this->last_operation_ == COVER_OPERATION_CLOSING) { + this->target_position_ = COVER_OPEN; + this->start_direction_(COVER_OPERATION_OPENING); + } else { + this->target_position_ = COVER_CLOSED; + this->start_direction_(COVER_OPERATION_CLOSING); + } + } + } if (call.get_position().has_value()) { auto pos = *call.get_position(); if (pos == this->position) { @@ -131,7 +147,7 @@ void CurrentBasedCover::dump_config() { ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f); ESP_LOGCONFIG(TAG, "Obstacle Rollback: %.1f%%", this->obstacle_rollback_ * 100); if (this->max_duration_ != UINT32_MAX) { - ESP_LOGCONFIG(TAG, "Maximun duration: %.1fs", this->max_duration_ / 1e3f); + ESP_LOGCONFIG(TAG, "Maximum duration: %.1fs", this->max_duration_ / 1e3f); } ESP_LOGCONFIG(TAG, "Start sensing delay: %.1fs", this->start_sensing_delay_ / 1e3f); ESP_LOGCONFIG(TAG, "Malfunction detection: %s", YESNO(this->malfunction_detection_)); @@ -164,7 +180,7 @@ bool CurrentBasedCover::is_closing_blocked_() const { if (this->close_obstacle_current_threshold_ == FLT_MAX) { return false; } - return this->open_sensor_->get_state() > this->open_obstacle_current_threshold_; + return this->close_sensor_->get_state() > this->close_obstacle_current_threshold_; } bool CurrentBasedCover::is_initial_delay_finished_() const { return millis() - this->start_dir_time_ > this->start_sensing_delay_; @@ -202,9 +218,11 @@ void CurrentBasedCover::start_direction_(CoverOperation dir) { trig = this->stop_trigger_; break; case COVER_OPERATION_OPENING: + this->last_operation_ = dir; trig = this->open_trigger_; break; case COVER_OPERATION_CLOSING: + this->last_operation_ = dir; trig = this->close_trigger_; break; default: diff --git a/esphome/components/current_based/current_based_cover.h b/esphome/components/current_based/current_based_cover.h index 220b770c05..b172e762b0 100644 --- a/esphome/components/current_based/current_based_cover.h +++ b/esphome/components/current_based/current_based_cover.h @@ -89,6 +89,8 @@ class CurrentBasedCover : public cover::Cover, public Component { uint32_t start_dir_time_{0}; uint32_t last_publish_time_{0}; float target_position_{0}; + + cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING}; }; } // namespace current_based diff --git a/esphome/components/custom/binary_sensor/__init__.py b/esphome/components/custom/binary_sensor/__init__.py index 18d613d4c1..8d6d621b3a 100644 --- a/esphome/components/custom/binary_sensor/__init__.py +++ b/esphome/components/custom/binary_sensor/__init__.py @@ -11,7 +11,7 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(): cv.declare_id(CustomBinarySensorConstructor), cv.Required(CONF_LAMBDA): cv.returning_lambda, cv.Required(CONF_BINARY_SENSORS): cv.ensure_list( - binary_sensor.BINARY_SENSOR_SCHEMA + binary_sensor.binary_sensor_schema() ), } ) diff --git a/esphome/components/custom/binary_sensor/custom_binary_sensor.h b/esphome/components/custom/binary_sensor/custom_binary_sensor.h index 314b9b0832..b7d5458d9e 100644 --- a/esphome/components/custom/binary_sensor/custom_binary_sensor.h +++ b/esphome/components/custom/binary_sensor/custom_binary_sensor.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "esphome/components/binary_sensor/binary_sensor.h" +#include + namespace esphome { namespace custom { diff --git a/esphome/components/custom/climate/custom_climate.h b/esphome/components/custom/climate/custom_climate.h index 250d83f69f..37876f7115 100644 --- a/esphome/components/custom/climate/custom_climate.h +++ b/esphome/components/custom/climate/custom_climate.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "esphome/components/climate/climate.h" +#include + namespace esphome { namespace custom { diff --git a/esphome/components/custom/cover/custom_cover.h b/esphome/components/custom/cover/custom_cover.h index 71f271bc86..58330b9d54 100644 --- a/esphome/components/custom/cover/custom_cover.h +++ b/esphome/components/custom/cover/custom_cover.h @@ -2,6 +2,8 @@ #include "esphome/components/cover/cover.h" +#include + namespace esphome { namespace custom { diff --git a/esphome/components/custom/light/custom_light_output.h b/esphome/components/custom/light/custom_light_output.h index 744e99b889..c2c83ebe97 100644 --- a/esphome/components/custom/light/custom_light_output.h +++ b/esphome/components/custom/light/custom_light_output.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "esphome/components/light/light_output.h" +#include + namespace esphome { namespace custom { diff --git a/esphome/components/custom/output/custom_output.h b/esphome/components/custom/output/custom_output.h index 1b55d51e29..4624642420 100644 --- a/esphome/components/custom/output/custom_output.h +++ b/esphome/components/custom/output/custom_output.h @@ -4,6 +4,8 @@ #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" +#include + namespace esphome { namespace custom { diff --git a/esphome/components/custom/sensor/__init__.py b/esphome/components/custom/sensor/__init__.py index bf9421e43e..be17d9a334 100644 --- a/esphome/components/custom/sensor/__init__.py +++ b/esphome/components/custom/sensor/__init__.py @@ -10,7 +10,7 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(CustomSensorConstructor), cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_SENSORS): cv.ensure_list(sensor.SENSOR_SCHEMA), + cv.Required(CONF_SENSORS): cv.ensure_list(sensor.sensor_schema()), } ) diff --git a/esphome/components/custom/sensor/custom_sensor.h b/esphome/components/custom/sensor/custom_sensor.h index 5ef3658e5d..d8f4fbc109 100644 --- a/esphome/components/custom/sensor/custom_sensor.h +++ b/esphome/components/custom/sensor/custom_sensor.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" +#include + namespace esphome { namespace custom { diff --git a/esphome/components/custom/switch/__init__.py b/esphome/components/custom/switch/__init__.py index e0b9d7751a..5538ae6aa0 100644 --- a/esphome/components/custom/switch/__init__.py +++ b/esphome/components/custom/switch/__init__.py @@ -10,13 +10,7 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(CustomSwitchConstructor), cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_SWITCHES): cv.ensure_list( - switch.SWITCH_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(switch.Switch), - } - ) - ), + cv.Required(CONF_SWITCHES): cv.ensure_list(switch.switch_schema(switch.Switch)), } ) diff --git a/esphome/components/custom/switch/custom_switch.h b/esphome/components/custom/switch/custom_switch.h index 186e7473fe..9657e4b44d 100644 --- a/esphome/components/custom/switch/custom_switch.h +++ b/esphome/components/custom/switch/custom_switch.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "esphome/components/switch/switch.h" +#include + namespace esphome { namespace custom { diff --git a/esphome/components/custom/text_sensor/__init__.py b/esphome/components/custom/text_sensor/__init__.py index 5b6d416436..70728af604 100644 --- a/esphome/components/custom/text_sensor/__init__.py +++ b/esphome/components/custom/text_sensor/__init__.py @@ -11,11 +11,7 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(): cv.declare_id(CustomTextSensorConstructor), cv.Required(CONF_LAMBDA): cv.returning_lambda, cv.Required(CONF_TEXT_SENSORS): cv.ensure_list( - text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ) + text_sensor.text_sensor_schema() ), } ) diff --git a/esphome/components/custom/text_sensor/custom_text_sensor.h b/esphome/components/custom/text_sensor/custom_text_sensor.h index f1e9c7665e..13732c00b6 100644 --- a/esphome/components/custom/text_sensor/custom_text_sensor.h +++ b/esphome/components/custom/text_sensor/custom_text_sensor.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "esphome/components/text_sensor/text_sensor.h" +#include + namespace esphome { namespace custom { diff --git a/esphome/components/custom_component/custom_component.h b/esphome/components/custom_component/custom_component.h index 3f5760e4cf..3b34019a7a 100644 --- a/esphome/components/custom_component/custom_component.h +++ b/esphome/components/custom_component/custom_component.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "esphome/core/application.h" +#include + namespace esphome { namespace custom_component { diff --git a/esphome/components/dac7678/__init__.py b/esphome/components/dac7678/__init__.py new file mode 100644 index 0000000000..b6cd2b384e --- /dev/null +++ b/esphome/components/dac7678/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID + +AUTO_LOAD = ["output"] +CODEOWNERS = ["@NickB1"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +dac7678_ns = cg.esphome_ns.namespace("dac7678") +DAC7678Output = dac7678_ns.class_("DAC7678Output", cg.Component, i2c.I2CDevice) +CONF_INTERNAL_REFERENCE = "internal_reference" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(DAC7678Output), + cv.Optional(CONF_INTERNAL_REFERENCE, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x48)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + cg.add(var.set_internal_reference(config[CONF_INTERNAL_REFERENCE])) + await i2c.register_i2c_device(var, config) + return var diff --git a/esphome/components/dac7678/dac7678_output.cpp b/esphome/components/dac7678/dac7678_output.cpp new file mode 100644 index 0000000000..b6de615b30 --- /dev/null +++ b/esphome/components/dac7678/dac7678_output.cpp @@ -0,0 +1,86 @@ +#include "dac7678_output.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace dac7678 { + +static const char *const TAG = "dac7678"; + +static const uint8_t DAC7678_REG_INPUT_N = 0x00; +static const uint8_t DAC7678_REG_SELECT_UPDATE_N = 0x10; +static const uint8_t DAC7678_REG_WRITE_N_UPDATE_ALL = 0x20; +static const uint8_t DAC7678_REG_WRITE_N_UPDATE_N = 0x30; +static const uint8_t DAC7678_REG_POWER = 0x40; +static const uint8_t DAC7678_REG_CLEAR_CODE = 0x50; +static const uint8_t DAC7678_REG_LDAC = 0x60; +static const uint8_t DAC7678_REG_SOFTWARE_RESET = 0x70; +static const uint8_t DAC7678_REG_INTERNAL_REF_0 = 0x80; +static const uint8_t DAC7678_REG_INTERNAL_REF_1 = 0x90; + +void DAC7678Output::setup() { + ESP_LOGCONFIG(TAG, "Setting up DAC7678OutputComponent..."); + + ESP_LOGV(TAG, "Resetting device..."); + + // Reset device + if (!this->write_byte_16(DAC7678_REG_SOFTWARE_RESET, 0x0000)) { + ESP_LOGE(TAG, "Reset failed"); + this->mark_failed(); + return; + } else { + ESP_LOGV(TAG, "Reset succeeded"); + } + + delayMicroseconds(1000); + + // Set internal reference + if (this->internal_reference_) { + if (!this->write_byte_16(DAC7678_REG_INTERNAL_REF_0, 1 << 4)) { + ESP_LOGE(TAG, "Set internal reference failed"); + this->mark_failed(); + return; + } else { + ESP_LOGV(TAG, "Internal reference enabled"); + } + } +} + +void DAC7678Output::dump_config() { + if (this->is_failed()) { + ESP_LOGE(TAG, "Setting up DAC7678 failed!"); + } else { + ESP_LOGCONFIG(TAG, "DAC7678 initialised"); + } +} + +void DAC7678Output::register_channel(DAC7678Channel *channel) { + auto c = channel->channel_; + this->min_channel_ = std::min(this->min_channel_, c); + this->max_channel_ = std::max(this->max_channel_, c); + channel->set_parent(this); + ESP_LOGV(TAG, "Registered channel: %01u", channel->channel_); +} + +void DAC7678Output::set_channel_value_(uint8_t channel, uint16_t value) { + if (this->dac_input_reg_[channel] != value) { + ESP_LOGV(TAG, "Channel %01u: input_reg=%04u ", channel, value); + + if (!this->write_byte_16(DAC7678_REG_WRITE_N_UPDATE_N | channel, value << 4)) { + this->status_set_warning(); + return; + } + } + this->dac_input_reg_[channel] = value; + this->status_clear_warning(); +} + +void DAC7678Channel::write_state(float state) { + const float input_rounded = roundf(state * this->full_scale_); + auto input = static_cast(input_rounded); + this->parent_->set_channel_value_(this->channel_, input); +} + +} // namespace dac7678 +} // namespace esphome diff --git a/esphome/components/dac7678/dac7678_output.h b/esphome/components/dac7678/dac7678_output.h new file mode 100644 index 0000000000..abd9875e4c --- /dev/null +++ b/esphome/components/dac7678/dac7678_output.h @@ -0,0 +1,55 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/output/float_output.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace dac7678 { + +class DAC7678Output; + +class DAC7678Channel : public output::FloatOutput, public Parented { + public: + void set_channel(uint8_t channel) { channel_ = channel; } + + protected: + friend class DAC7678Output; + + const uint16_t full_scale_ = 0xFFF; + + void write_state(float state) override; + + uint8_t channel_; +}; + +/// DAC7678 float output component. +class DAC7678Output : public Component, public i2c::I2CDevice { + public: + DAC7678Output() {} + + void register_channel(DAC7678Channel *channel); + + void set_internal_reference(const bool value) { this->internal_reference_ = value; } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + protected: + friend DAC7678Channel; + + bool internal_reference_; + + void set_channel_value_(uint8_t channel, uint16_t value); + + uint8_t min_channel_{0xFF}; + uint8_t max_channel_{0x00}; + uint16_t dac_input_reg_[8] = { + 0, + }; +}; + +} // namespace dac7678 +} // namespace esphome diff --git a/esphome/components/dac7678/output.py b/esphome/components/dac7678/output.py new file mode 100644 index 0000000000..f41e5c2422 --- /dev/null +++ b/esphome/components/dac7678/output.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID +from . import DAC7678Output, dac7678_ns + +DEPENDENCIES = ["dac7678"] + +DAC7678Channel = dac7678_ns.class_("DAC7678Channel", output.FloatOutput) +CONF_DAC7678_ID = "dac7678_id" + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(DAC7678Channel), + cv.GenerateID(CONF_DAC7678_ID): cv.use_id(DAC7678Output), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7), + } +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_DAC7678_ID]) + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_channel(config[CONF_CHANNEL])) + cg.add(paren.register_channel(var)) + await output.register_output(var, config) + return var diff --git a/esphome/components/daikin_brc/__init__.py b/esphome/components/daikin_brc/__init__.py new file mode 100644 index 0000000000..69002c015f --- /dev/null +++ b/esphome/components/daikin_brc/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@hagak"] diff --git a/esphome/components/daikin_brc/climate.py b/esphome/components/daikin_brc/climate.py new file mode 100644 index 0000000000..3468b6533c --- /dev/null +++ b/esphome/components/daikin_brc/climate.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ["climate_ir"] + +daikin_brc_ns = cg.esphome_ns.namespace("daikin_brc") +DaikinBrcClimate = daikin_brc_ns.class_("DaikinBrcClimate", climate_ir.ClimateIR) + +CONF_USE_FAHRENHEIT = "use_fahrenheit" + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DaikinBrcClimate), + cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await climate_ir.register_climate_ir(var, config) + cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT])) diff --git a/esphome/components/daikin_brc/daikin_brc.cpp b/esphome/components/daikin_brc/daikin_brc.cpp new file mode 100644 index 0000000000..6683d70f80 --- /dev/null +++ b/esphome/components/daikin_brc/daikin_brc.cpp @@ -0,0 +1,273 @@ +#include "daikin_brc.h" +#include "esphome/components/remote_base/remote_base.h" + +namespace esphome { +namespace daikin_brc { + +static const char *const TAG = "daikin_brc.climate"; + +void DaikinBrcClimate::control(const climate::ClimateCall &call) { + this->mode_button_ = 0x00; + if (call.get_mode().has_value()) { + // Need to determine if this is call due to Mode button pressed so that we can set the Mode button byte + this->mode_button_ = DAIKIN_BRC_IR_MODE_BUTTON; + } + ClimateIR::control(call); +} + +void DaikinBrcClimate::transmit_state() { + uint8_t remote_state[DAIKIN_BRC_TRANSMIT_FRAME_SIZE] = {0x11, 0xDA, 0x17, 0x18, 0x04, 0x00, 0x1E, 0x11, + 0xDA, 0x17, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00}; + + remote_state[12] = this->alt_mode_(); + remote_state[13] = this->mode_button_; + remote_state[14] = this->operation_mode_(); + remote_state[17] = this->temperature_(); + remote_state[18] = this->fan_speed_swing_(); + + // Calculate checksum + for (int i = DAIKIN_BRC_PREAMBLE_SIZE; i < DAIKIN_BRC_TRANSMIT_FRAME_SIZE - 1; i++) { + remote_state[DAIKIN_BRC_TRANSMIT_FRAME_SIZE - 1] += remote_state[i]; + } + + auto transmit = this->transmitter_->transmit(); + auto *data = transmit.get_data(); + data->set_carrier_frequency(DAIKIN_BRC_IR_FREQUENCY); + + data->mark(DAIKIN_BRC_HEADER_MARK); + data->space(DAIKIN_BRC_HEADER_SPACE); + for (int i = 0; i < DAIKIN_BRC_PREAMBLE_SIZE; i++) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(DAIKIN_BRC_BIT_MARK); + bool bit = remote_state[i] & mask; + data->space(bit ? DAIKIN_BRC_ONE_SPACE : DAIKIN_BRC_ZERO_SPACE); + } + } + data->mark(DAIKIN_BRC_BIT_MARK); + data->space(DAIKIN_BRC_MESSAGE_SPACE); + data->mark(DAIKIN_BRC_HEADER_MARK); + data->space(DAIKIN_BRC_HEADER_SPACE); + + for (int i = DAIKIN_BRC_PREAMBLE_SIZE; i < DAIKIN_BRC_TRANSMIT_FRAME_SIZE; i++) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(DAIKIN_BRC_BIT_MARK); + bool bit = remote_state[i] & mask; + data->space(bit ? DAIKIN_BRC_ONE_SPACE : DAIKIN_BRC_ZERO_SPACE); + } + } + + data->mark(DAIKIN_BRC_BIT_MARK); + data->space(0); + + transmit.perform(); +} + +uint8_t DaikinBrcClimate::alt_mode_() { + uint8_t alt_mode = 0x00; + switch (this->mode) { + case climate::CLIMATE_MODE_DRY: + alt_mode = 0x23; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + alt_mode = 0x63; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + case climate::CLIMATE_MODE_COOL: + case climate::CLIMATE_MODE_HEAT: + default: + alt_mode = 0x73; + break; + } + return alt_mode; +} + +uint8_t DaikinBrcClimate::operation_mode_() { + uint8_t operating_mode = DAIKIN_BRC_MODE_ON; + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + operating_mode |= DAIKIN_BRC_MODE_COOL; + break; + case climate::CLIMATE_MODE_DRY: + operating_mode |= DAIKIN_BRC_MODE_DRY; + break; + case climate::CLIMATE_MODE_HEAT: + operating_mode |= DAIKIN_BRC_MODE_HEAT; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + operating_mode |= DAIKIN_BRC_MODE_AUTO; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + operating_mode |= DAIKIN_BRC_MODE_FAN; + break; + case climate::CLIMATE_MODE_OFF: + default: + operating_mode = DAIKIN_BRC_MODE_OFF; + break; + } + + return operating_mode; +} + +uint8_t DaikinBrcClimate::fan_speed_swing_() { + uint16_t fan_speed; + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + fan_speed = DAIKIN_BRC_FAN_1; + break; + case climate::CLIMATE_FAN_MEDIUM: + fan_speed = DAIKIN_BRC_FAN_2; + break; + case climate::CLIMATE_FAN_HIGH: + fan_speed = DAIKIN_BRC_FAN_3; + break; + default: + fan_speed = DAIKIN_BRC_FAN_1; + } + + // If swing is enabled switch first 4 bits to 1111 + switch (this->swing_mode) { + case climate::CLIMATE_SWING_BOTH: + fan_speed |= DAIKIN_BRC_IR_SWING_ON; + break; + default: + fan_speed |= DAIKIN_BRC_IR_SWING_OFF; + break; + } + return fan_speed; +} + +uint8_t DaikinBrcClimate::temperature_() { + // Force special temperatures depending on the mode + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + case climate::CLIMATE_MODE_DRY: + if (this->fahrenheit_) { + return DAIKIN_BRC_IR_DRY_FAN_TEMP_F; + } + return DAIKIN_BRC_IR_DRY_FAN_TEMP_C; + case climate::CLIMATE_MODE_HEAT_COOL: + default: + uint8_t temperature; + // Temperature in remote is in F + if (this->fahrenheit_) { + temperature = (uint8_t) roundf( + clamp(((this->target_temperature * 1.8) + 32), DAIKIN_BRC_TEMP_MIN_F, DAIKIN_BRC_TEMP_MAX_F)); + } else { + temperature = ((uint8_t) roundf(this->target_temperature) - 9) << 1; + } + return temperature; + } +} + +bool DaikinBrcClimate::parse_state_frame_(const uint8_t frame[]) { + uint8_t checksum = 0; + for (int i = 0; i < (DAIKIN_BRC_STATE_FRAME_SIZE - 1); i++) { + checksum += frame[i]; + } + if (frame[DAIKIN_BRC_STATE_FRAME_SIZE - 1] != checksum) { + ESP_LOGCONFIG(TAG, "Bad CheckSum %x", checksum); + return false; + } + + uint8_t mode = frame[7]; + if (mode & DAIKIN_BRC_MODE_ON) { + switch (mode & 0xF0) { + case DAIKIN_BRC_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case DAIKIN_BRC_MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case DAIKIN_BRC_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case DAIKIN_BRC_MODE_AUTO: + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + break; + case DAIKIN_BRC_MODE_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + } + } else { + this->mode = climate::CLIMATE_MODE_OFF; + } + + uint8_t temperature = frame[10]; + float temperature_c; + if (this->fahrenheit_) { + temperature_c = clamp(((temperature - 32) / 1.8), DAIKIN_BRC_TEMP_MIN_C, DAIKIN_BRC_TEMP_MAX_C); + } else { + temperature_c = (temperature >> 1) + 9; + } + + this->target_temperature = temperature_c; + + uint8_t fan_mode = frame[11]; + uint8_t swing_mode = frame[11]; + switch (swing_mode & 0xF) { + case DAIKIN_BRC_IR_SWING_ON: + this->swing_mode = climate::CLIMATE_SWING_BOTH; + break; + case DAIKIN_BRC_IR_SWING_OFF: + this->swing_mode = climate::CLIMATE_SWING_OFF; + break; + } + + switch (fan_mode & 0xF0) { + case DAIKIN_BRC_FAN_1: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case DAIKIN_BRC_FAN_2: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case DAIKIN_BRC_FAN_3: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + } + this->publish_state(); + return true; +} + +bool DaikinBrcClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t state_frame[DAIKIN_BRC_STATE_FRAME_SIZE] = {}; + if (!data.expect_item(DAIKIN_BRC_HEADER_MARK, DAIKIN_BRC_HEADER_SPACE)) { + return false; + } + for (uint8_t pos = 0; pos < DAIKIN_BRC_STATE_FRAME_SIZE; pos++) { + uint8_t byte = 0; + for (int8_t bit = 0; bit < 8; bit++) { + if (data.expect_item(DAIKIN_BRC_BIT_MARK, DAIKIN_BRC_ONE_SPACE)) { + byte |= 1 << bit; + } else if (!data.expect_item(DAIKIN_BRC_BIT_MARK, DAIKIN_BRC_ZERO_SPACE)) { + return false; + } + } + state_frame[pos] = byte; + if (pos == 0) { + // frame header + if (byte != 0x11) + return false; + } else if (pos == 1) { + // frame header + if (byte != 0xDA) + return false; + } else if (pos == 2) { + // frame header + if (byte != 0x17) + return false; + } else if (pos == 3) { + // frame header + if (byte != 0x18) + return false; + } else if (pos == 4) { + // frame type + if (byte != 0x00) + return false; + } + } + return this->parse_state_frame_(state_frame); +} + +} // namespace daikin_brc +} // namespace esphome diff --git a/esphome/components/daikin_brc/daikin_brc.h b/esphome/components/daikin_brc/daikin_brc.h new file mode 100644 index 0000000000..bdc6384809 --- /dev/null +++ b/esphome/components/daikin_brc/daikin_brc.h @@ -0,0 +1,82 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace daikin_brc { + +// Values for Daikin BRC4CXXX IR Controllers +// Temperature +const uint8_t DAIKIN_BRC_TEMP_MIN_F = 60; // fahrenheit +const uint8_t DAIKIN_BRC_TEMP_MAX_F = 90; // fahrenheit +const float DAIKIN_BRC_TEMP_MIN_C = (DAIKIN_BRC_TEMP_MIN_F - 32) / 1.8; // fahrenheit +const float DAIKIN_BRC_TEMP_MAX_C = (DAIKIN_BRC_TEMP_MAX_F - 32) / 1.8; // fahrenheit + +// Modes +const uint8_t DAIKIN_BRC_MODE_AUTO = 0x30; +const uint8_t DAIKIN_BRC_MODE_COOL = 0x20; +const uint8_t DAIKIN_BRC_MODE_HEAT = 0x10; +const uint8_t DAIKIN_BRC_MODE_DRY = 0x70; +const uint8_t DAIKIN_BRC_MODE_FAN = 0x00; +const uint8_t DAIKIN_BRC_MODE_OFF = 0x00; +const uint8_t DAIKIN_BRC_MODE_ON = 0x01; + +// Fan Speed +const uint8_t DAIKIN_BRC_FAN_1 = 0x10; +const uint8_t DAIKIN_BRC_FAN_2 = 0x30; +const uint8_t DAIKIN_BRC_FAN_3 = 0x50; +const uint8_t DAIKIN_BRC_FAN_AUTO = 0xA0; + +// IR Transmission +const uint32_t DAIKIN_BRC_IR_FREQUENCY = 38000; +const uint32_t DAIKIN_BRC_HEADER_MARK = 5070; +const uint32_t DAIKIN_BRC_HEADER_SPACE = 2140; +const uint32_t DAIKIN_BRC_BIT_MARK = 370; +const uint32_t DAIKIN_BRC_ONE_SPACE = 1780; +const uint32_t DAIKIN_BRC_ZERO_SPACE = 710; +const uint32_t DAIKIN_BRC_MESSAGE_SPACE = 29410; + +const uint8_t DAIKIN_BRC_IR_DRY_FAN_TEMP_F = 72; // Dry/Fan mode is always 17 Celsius. +const uint8_t DAIKIN_BRC_IR_DRY_FAN_TEMP_C = (17 - 9) * 2; // Dry/Fan mode is always 17 Celsius. +const uint8_t DAIKIN_BRC_IR_SWING_ON = 0x5; +const uint8_t DAIKIN_BRC_IR_SWING_OFF = 0x6; +const uint8_t DAIKIN_BRC_IR_MODE_BUTTON = 0x4; // This is set after a mode action + +// State Frame size +const uint8_t DAIKIN_BRC_STATE_FRAME_SIZE = 15; +// Preamble size +const uint8_t DAIKIN_BRC_PREAMBLE_SIZE = 7; +// Transmit Frame size - includes a preamble +const uint8_t DAIKIN_BRC_TRANSMIT_FRAME_SIZE = DAIKIN_BRC_PREAMBLE_SIZE + DAIKIN_BRC_STATE_FRAME_SIZE; + +class DaikinBrcClimate : public climate_ir::ClimateIR { + public: + DaikinBrcClimate() + : climate_ir::ClimateIR(DAIKIN_BRC_TEMP_MIN_C, DAIKIN_BRC_TEMP_MAX_C, 0.5f, true, true, + {climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH}) {} + + /// Set use of Fahrenheit units + void set_fahrenheit(bool value) { + this->fahrenheit_ = value; + this->temperature_step_ = value ? 0.5f : 1.0f; + } + + protected: + uint8_t mode_button_ = 0x00; + // Capture if the MODE was changed + void control(const climate::ClimateCall &call) override; + // Transmit via IR the state of this climate controller. + void transmit_state() override; + uint8_t alt_mode_(); + uint8_t operation_mode_(); + uint8_t fan_speed_swing_(); + uint8_t temperature_(); + // Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + bool parse_state_frame_(const uint8_t frame[]); + bool fahrenheit_{false}; +}; + +} // namespace daikin_brc +} // namespace esphome diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp index 1eed2ebf78..302422d6c7 100644 --- a/esphome/components/dallas/dallas_component.cpp +++ b/esphome/components/dallas/dallas_component.cpp @@ -32,6 +32,11 @@ void DallasComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up DallasComponent..."); pin_->setup(); + + // clear bus with 480µs high, otherwise initial reset in search_vec() fails + pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + delayMicroseconds(480); + one_wire_ = new ESPOneWire(pin_); // NOLINT(cppcoreguidelines-owning-memory) std::vector raw_sensors; @@ -99,20 +104,25 @@ void DallasComponent::update() { this->status_clear_warning(); bool result; - if (!this->one_wire_->reset()) { - result = false; - } else { - result = true; - this->one_wire_->skip(); - this->one_wire_->write8(DALLAS_COMMAND_START_CONVERSION); + { + InterruptLock lock; + result = this->one_wire_->reset(); } - if (!result) { ESP_LOGE(TAG, "Requesting conversion failed"); this->status_set_warning(); + for (auto *sensor : this->sensors_) { + sensor->publish_state(NAN); + } return; } + { + InterruptLock lock; + this->one_wire_->skip(); + this->one_wire_->write8(DALLAS_COMMAND_START_CONVERSION); + } + for (auto *sensor : this->sensors_) { this->set_timeout(sensor->get_address_name(), sensor->millis_to_wait_for_conversion(), [this, sensor] { bool res = sensor->read_scratch_pad(); @@ -124,7 +134,6 @@ void DallasComponent::update() { return; } if (!sensor->check_scratch_pad()) { - ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", sensor->get_name().c_str()); sensor->publish_state(NAN); this->status_set_warning(); return; @@ -152,16 +161,26 @@ const std::string &DallasTemperatureSensor::get_address_name() { } bool IRAM_ATTR DallasTemperatureSensor::read_scratch_pad() { auto *wire = this->parent_->one_wire_; - if (!wire->reset()) { - return false; + + { + InterruptLock lock; + + if (!wire->reset()) { + return false; + } } - wire->select(this->address_); - wire->write8(DALLAS_COMMAND_READ_SCRATCH_PAD); + { + InterruptLock lock; - for (unsigned char &i : this->scratch_pad_) { - i = wire->read8(); + wire->select(this->address_); + wire->write8(DALLAS_COMMAND_READ_SCRATCH_PAD); + + for (unsigned char &i : this->scratch_pad_) { + i = wire->read8(); + } } + return true; } bool DallasTemperatureSensor::setup_sensor() { @@ -200,17 +219,20 @@ bool DallasTemperatureSensor::setup_sensor() { } auto *wire = this->parent_->one_wire_; - if (wire->reset()) { - wire->select(this->address_); - wire->write8(DALLAS_COMMAND_WRITE_SCRATCH_PAD); - wire->write8(this->scratch_pad_[2]); // high alarm temp - wire->write8(this->scratch_pad_[3]); // low alarm temp - wire->write8(this->scratch_pad_[4]); // resolution - wire->reset(); + { + InterruptLock lock; + if (wire->reset()) { + wire->select(this->address_); + wire->write8(DALLAS_COMMAND_WRITE_SCRATCH_PAD); + wire->write8(this->scratch_pad_[2]); // high alarm temp + wire->write8(this->scratch_pad_[3]); // low alarm temp + wire->write8(this->scratch_pad_[4]); // resolution + wire->reset(); - // write value to EEPROM - wire->select(this->address_); - wire->write8(0x48); + // write value to EEPROM + wire->select(this->address_); + wire->write8(0x48); + } } delay(20); // allow it to finish operation @@ -218,13 +240,29 @@ bool DallasTemperatureSensor::setup_sensor() { return true; } bool DallasTemperatureSensor::check_scratch_pad() { + bool chksum_validity = (crc8(this->scratch_pad_, 8) == this->scratch_pad_[8]); + bool config_validity = false; + + switch (this->get_address8()[0]) { + case DALLAS_MODEL_DS18B20: + config_validity = ((this->scratch_pad_[4] & 0x9F) == 0x1F); + break; + default: + config_validity = ((this->scratch_pad_[4] & 0x10) == 0x10); + } + #ifdef ESPHOME_LOG_LEVEL_VERY_VERBOSE ESP_LOGVV(TAG, "Scratch pad: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X (%02X)", this->scratch_pad_[0], this->scratch_pad_[1], this->scratch_pad_[2], this->scratch_pad_[3], this->scratch_pad_[4], this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8], crc8(this->scratch_pad_, 8)); #endif - return crc8(this->scratch_pad_, 8) == this->scratch_pad_[8]; + if (!chksum_validity) { + ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", this->get_name().c_str()); + } else if (!config_validity) { + ESP_LOGW(TAG, "'%s' - Scratch pad config register invalid!", this->get_name().c_str()); + } + return chksum_validity && config_validity; } float DallasTemperatureSensor::get_temp_c() { int16_t temp = (int16_t(this->scratch_pad_[1]) << 11) | (int16_t(this->scratch_pad_[0]) << 3); diff --git a/esphome/components/dallas/dallas_component.h b/esphome/components/dallas/dallas_component.h index 37c098283a..b21bc02e54 100644 --- a/esphome/components/dallas/dallas_component.h +++ b/esphome/components/dallas/dallas_component.h @@ -4,6 +4,8 @@ #include "esphome/components/sensor/sensor.h" #include "esp_one_wire.h" +#include + namespace esphome { namespace dallas { diff --git a/esphome/components/dallas/esp_one_wire.cpp b/esphome/components/dallas/esp_one_wire.cpp index 6dc085a0bf..32ddf07fb6 100644 --- a/esphome/components/dallas/esp_one_wire.cpp +++ b/esphome/components/dallas/esp_one_wire.cpp @@ -15,8 +15,6 @@ ESPOneWire::ESPOneWire(InternalGPIOPin *pin) { pin_ = pin->to_isr(); } bool HOT IRAM_ATTR ESPOneWire::reset() { // See reset here: // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html - InterruptLock lock; - // Wait for communication to clear (delay G) pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); uint8_t retries = 125; @@ -43,16 +41,18 @@ bool HOT IRAM_ATTR ESPOneWire::reset() { } void HOT IRAM_ATTR ESPOneWire::write_bit(bool bit) { - // See write 1/0 bit here: - // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html - InterruptLock lock; - // drive bus low pin_.pin_mode(gpio::FLAG_OUTPUT); pin_.digital_write(false); - uint32_t delay0 = bit ? 10 : 65; - uint32_t delay1 = bit ? 55 : 5; + // from datasheet: + // write 0 low time: t_low0: min=60µs, max=120µs + // write 1 low time: t_low1: min=1µs, max=15µs + // time slot: t_slot: min=60µs, max=120µs + // recovery time: t_rec: min=1µs + // ds18b20 appears to read the bus after roughly 14µs + uint32_t delay0 = bit ? 6 : 60; + uint32_t delay1 = bit ? 54 : 5; // delay A/C delayMicroseconds(delay0); @@ -63,72 +63,97 @@ void HOT IRAM_ATTR ESPOneWire::write_bit(bool bit) { } bool HOT IRAM_ATTR ESPOneWire::read_bit() { - // See read bit here: - // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html - InterruptLock lock; - - // drive bus low, delay A + // drive bus low pin_.pin_mode(gpio::FLAG_OUTPUT); pin_.digital_write(false); + + // note: for reading we'll need very accurate timing, as the + // timing for the digital_read() is tight; according to the datasheet, + // we should read at the end of 16µs starting from the bus low + // typically, the ds18b20 pulls the line high after 11µs for a logical 1 + // and 29µs for a logical 0 + + uint32_t start = micros(); + // datasheet says >1µs delayMicroseconds(3); // release bus, delay E pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); - delayMicroseconds(10); + + // Unfortunately some frameworks have different characteristics than others + // esp32 arduino appears to pull the bus low only after the digital_write(false), + // whereas on esp-idf it already happens during the pin_mode(OUTPUT) + // manually correct for this with these constants. + +#ifdef USE_ESP32 + uint32_t timing_constant = 12; +#else + uint32_t timing_constant = 14; +#endif + + // measure from start value directly, to get best accurate timing no matter + // how long pin_mode/delayMicroseconds took + while (micros() - start < timing_constant) + ; // sample bus to read bit from peer bool r = pin_.digital_read(); - // delay F - delayMicroseconds(53); + // read slot is at least 60µs; get as close to 60µs to spend less time with interrupts locked + uint32_t now = micros(); + if (now - start < 60) + delayMicroseconds(60 - (now - start)); + return r; } -void ESPOneWire::write8(uint8_t val) { +void IRAM_ATTR ESPOneWire::write8(uint8_t val) { for (uint8_t i = 0; i < 8; i++) { this->write_bit(bool((1u << i) & val)); } } -void ESPOneWire::write64(uint64_t val) { +void IRAM_ATTR ESPOneWire::write64(uint64_t val) { for (uint8_t i = 0; i < 64; i++) { this->write_bit(bool((1ULL << i) & val)); } } -uint8_t ESPOneWire::read8() { +uint8_t IRAM_ATTR ESPOneWire::read8() { uint8_t ret = 0; for (uint8_t i = 0; i < 8; i++) { ret |= (uint8_t(this->read_bit()) << i); } return ret; } -uint64_t ESPOneWire::read64() { +uint64_t IRAM_ATTR ESPOneWire::read64() { uint64_t ret = 0; for (uint8_t i = 0; i < 8; i++) { ret |= (uint64_t(this->read_bit()) << i); } return ret; } -void ESPOneWire::select(uint64_t address) { +void IRAM_ATTR ESPOneWire::select(uint64_t address) { this->write8(ONE_WIRE_ROM_SELECT); this->write64(address); } -void ESPOneWire::reset_search() { +void IRAM_ATTR ESPOneWire::reset_search() { this->last_discrepancy_ = 0; this->last_device_flag_ = false; - this->last_family_discrepancy_ = 0; this->rom_number_ = 0; } -uint64_t ESPOneWire::search() { +uint64_t IRAM_ATTR ESPOneWire::search() { if (this->last_device_flag_) { return 0u; } - if (!this->reset()) { - // Reset failed or no devices present - this->reset_search(); - return 0u; + { + InterruptLock lock; + if (!this->reset()) { + // Reset failed or no devices present + this->reset_search(); + return 0u; + } } uint8_t id_bit_number = 1; @@ -137,58 +162,58 @@ uint64_t ESPOneWire::search() { bool search_result = false; uint8_t rom_byte_mask = 1; - // Initiate search - this->write8(ONE_WIRE_ROM_SEARCH); - do { - // read bit - bool id_bit = this->read_bit(); - // read its complement - bool cmp_id_bit = this->read_bit(); + { + InterruptLock lock; + // Initiate search + this->write8(ONE_WIRE_ROM_SEARCH); + do { + // read bit + bool id_bit = this->read_bit(); + // read its complement + bool cmp_id_bit = this->read_bit(); - if (id_bit && cmp_id_bit) { - // No devices participating in search - break; - } - - bool branch; - - if (id_bit != cmp_id_bit) { - // only chose one branch, the other one doesn't have any devices. - branch = id_bit; - } else { - // there are devices with both 0s and 1s at this bit - if (id_bit_number < this->last_discrepancy_) { - branch = (this->rom_number8_()[rom_byte_number] & rom_byte_mask) > 0; - } else { - branch = id_bit_number == this->last_discrepancy_; + if (id_bit && cmp_id_bit) { + // No devices participating in search + break; } - if (!branch) { - last_zero = id_bit_number; - if (last_zero < 9) { - this->last_discrepancy_ = last_zero; + bool branch; + + if (id_bit != cmp_id_bit) { + // only chose one branch, the other one doesn't have any devices. + branch = id_bit; + } else { + // there are devices with both 0s and 1s at this bit + if (id_bit_number < this->last_discrepancy_) { + branch = (this->rom_number8_()[rom_byte_number] & rom_byte_mask) > 0; + } else { + branch = id_bit_number == this->last_discrepancy_; + } + + if (!branch) { + last_zero = id_bit_number; } } - } - if (branch) { - // set bit - this->rom_number8_()[rom_byte_number] |= rom_byte_mask; - } else { - // clear bit - this->rom_number8_()[rom_byte_number] &= ~rom_byte_mask; - } + if (branch) { + // set bit + this->rom_number8_()[rom_byte_number] |= rom_byte_mask; + } else { + // clear bit + this->rom_number8_()[rom_byte_number] &= ~rom_byte_mask; + } - // choose/announce branch - this->write_bit(branch); - id_bit_number++; - rom_byte_mask <<= 1; - if (rom_byte_mask == 0u) { - // go to next byte - rom_byte_number++; - rom_byte_mask = 1; - } - } while (rom_byte_number < 8); // loop through all bytes + // choose/announce branch + this->write_bit(branch); + id_bit_number++; + rom_byte_mask <<= 1; + if (rom_byte_mask == 0u) { + // go to next byte + rom_byte_number++; + rom_byte_mask = 1; + } + } while (rom_byte_number < 8); // loop through all bytes + } if (id_bit_number >= 65) { this->last_discrepancy_ = last_zero; @@ -217,7 +242,7 @@ std::vector ESPOneWire::search_vec() { return res; } -void ESPOneWire::skip() { +void IRAM_ATTR ESPOneWire::skip() { this->write8(0xCC); // skip ROM } diff --git a/esphome/components/dallas/esp_one_wire.h b/esphome/components/dallas/esp_one_wire.h index ef6f079f02..7544a6fe98 100644 --- a/esphome/components/dallas/esp_one_wire.h +++ b/esphome/components/dallas/esp_one_wire.h @@ -60,7 +60,6 @@ class ESPOneWire { ISRInternalGPIOPin pin_; uint8_t last_discrepancy_{0}; - uint8_t last_family_discrepancy_{0}; bool last_device_flag_{false}; uint64_t rom_number_{0}; }; diff --git a/esphome/components/dallas/sensor.py b/esphome/components/dallas/sensor.py index 14ad0efa7b..9288f0a3a6 100644 --- a/esphome/components/dallas/sensor.py +++ b/esphome/components/dallas/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - CONF_ID, ) from . import DallasComponent, dallas_ns @@ -17,13 +16,13 @@ DallasTemperatureSensor = dallas_ns.class_("DallasTemperatureSensor", sensor.Sen CONFIG_SCHEMA = cv.All( sensor.sensor_schema( + DallasTemperatureSensor, unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ).extend( { - cv.GenerateID(): cv.declare_id(DallasTemperatureSensor), cv.GenerateID(CONF_DALLAS_ID): cv.use_id(DallasComponent), cv.Optional(CONF_ADDRESS): cv.hex_int, cv.Optional(CONF_INDEX): cv.positive_int, @@ -36,7 +35,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): hub = await cg.get_variable(config[CONF_DALLAS_ID]) - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) if CONF_ADDRESS in config: cg.add(var.set_address(config[CONF_ADDRESS])) @@ -49,4 +48,3 @@ async def to_code(config): cg.add(var.set_parent(hub)) cg.add(hub.register_sensor(var)) - await sensor.register_sensor(var, config) diff --git a/esphome/components/daly_bms/__init__.py b/esphome/components/daly_bms/__init__.py index 45b8f98f0c..ce0cf5216a 100644 --- a/esphome/components/daly_bms/__init__.py +++ b/esphome/components/daly_bms/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import uart -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_ADDRESS CODEOWNERS = ["@s1lvi0"] DEPENDENCIES = ["uart"] @@ -15,7 +15,12 @@ DalyBmsComponent = daly_bms.class_( ) CONFIG_SCHEMA = ( - cv.Schema({cv.GenerateID(): cv.declare_id(DalyBmsComponent)}) + cv.Schema( + { + cv.GenerateID(): cv.declare_id(DalyBmsComponent), + cv.Optional(CONF_ADDRESS, default=0x80): cv.positive_int, + } + ) .extend(uart.UART_DEVICE_SCHEMA) .extend(cv.polling_component_schema("30s")) ) @@ -25,3 +30,4 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) + cg.add(var.set_address(config[CONF_ADDRESS])) diff --git a/esphome/components/daly_bms/binary_sensor.py b/esphome/components/daly_bms/binary_sensor.py index 23330cd945..7b252b5e89 100644 --- a/esphome/components/daly_bms/binary_sensor.py +++ b/esphome/components/daly_bms/binary_sensor.py @@ -1,7 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import CONF_ID from . import DalyBmsComponent, CONF_BMS_DALY_ID CONF_CHARGING_MOS_ENABLED = "charging_mos_enabled" @@ -18,18 +17,10 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(CONF_BMS_DALY_ID): cv.use_id(DalyBmsComponent), cv.Optional( CONF_CHARGING_MOS_ENABLED - ): binary_sensor.BINARY_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(binary_sensor.BinarySensor), - } - ), + ): binary_sensor.binary_sensor_schema(), cv.Optional( CONF_DISCHARGING_MOS_ENABLED - ): binary_sensor.BINARY_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(binary_sensor.BinarySensor), - } - ), + ): binary_sensor.binary_sensor_schema(), } ).extend(cv.COMPONENT_SCHEMA) ) @@ -38,9 +29,8 @@ CONFIG_SCHEMA = cv.All( async def setup_conf(config, key, hub): if key in config: conf = config[key] - sens = cg.new_Pvariable(conf[CONF_ID]) - await binary_sensor.register_binary_sensor(sens, conf) - cg.add(getattr(hub, f"set_{key}_binary_sensor")(sens)) + var = await binary_sensor.new_binary_sensor(conf) + cg.add(getattr(hub, f"set_{key}_binary_sensor")(var)) async def to_code(config): diff --git a/esphome/components/daly_bms/daly_bms.cpp b/esphome/components/daly_bms/daly_bms.cpp index 44c05f0686..3b41723327 100644 --- a/esphome/components/daly_bms/daly_bms.cpp +++ b/esphome/components/daly_bms/daly_bms.cpp @@ -16,6 +16,7 @@ static const uint8_t DALY_REQUEST_MIN_MAX_VOLTAGE = 0x91; static const uint8_t DALY_REQUEST_MIN_MAX_TEMPERATURE = 0x92; static const uint8_t DALY_REQUEST_MOS = 0x93; static const uint8_t DALY_REQUEST_STATUS = 0x94; +static const uint8_t DALY_REQUEST_CELL_VOLTAGE = 0x95; static const uint8_t DALY_REQUEST_TEMPERATURE = 0x96; void DalyBmsComponent::setup() {} @@ -31,6 +32,7 @@ void DalyBmsComponent::update() { this->request_data_(DALY_REQUEST_MIN_MAX_TEMPERATURE); this->request_data_(DALY_REQUEST_MOS); this->request_data_(DALY_REQUEST_STATUS); + this->request_data_(DALY_REQUEST_CELL_VOLTAGE); this->request_data_(DALY_REQUEST_TEMPERATURE); std::vector get_battery_level_data; @@ -48,7 +50,7 @@ void DalyBmsComponent::request_data_(uint8_t data_id) { uint8_t request_message[DALY_FRAME_SIZE]; request_message[0] = 0xA5; // Start Flag - request_message[1] = 0x80; // Communication Module Address + request_message[1] = addr_; // Communication Module Address request_message[2] = data_id; // Data ID request_message[3] = 0x08; // Data Length (Fixed) request_message[4] = 0x00; // Empty Data @@ -59,8 +61,8 @@ void DalyBmsComponent::request_data_(uint8_t data_id) { request_message[9] = 0x00; // | request_message[10] = 0x00; // | request_message[11] = 0x00; // Empty Data - request_message[12] = (uint8_t)(request_message[0] + request_message[1] + request_message[2] + - request_message[3]); // Checksum (Lower byte of the other bytes sum) + request_message[12] = (uint8_t) (request_message[0] + request_message[1] + request_message[2] + + request_message[3]); // Checksum (Lower byte of the other bytes sum) this->write_array(request_message, sizeof(request_message)); this->flush(); @@ -166,6 +168,71 @@ void DalyBmsComponent::decode_data_(std::vector data) { } break; + case DALY_REQUEST_CELL_VOLTAGE: + switch (it[4]) { + case 1: + if (this->cell_1_voltage_) { + this->cell_1_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); + } + if (this->cell_2_voltage_) { + this->cell_2_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); + } + if (this->cell_3_voltage_) { + this->cell_3_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); + } + break; + case 2: + if (this->cell_4_voltage_) { + this->cell_4_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); + } + if (this->cell_5_voltage_) { + this->cell_5_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); + } + if (this->cell_6_voltage_) { + this->cell_6_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); + } + break; + case 3: + if (this->cell_7_voltage_) { + this->cell_7_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); + } + if (this->cell_8_voltage_) { + this->cell_8_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); + } + if (this->cell_9_voltage_) { + this->cell_9_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); + } + break; + case 4: + if (this->cell_10_voltage_) { + this->cell_10_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); + } + if (this->cell_11_voltage_) { + this->cell_11_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); + } + if (this->cell_12_voltage_) { + this->cell_12_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); + } + break; + case 5: + if (this->cell_13_voltage_) { + this->cell_13_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); + } + if (this->cell_14_voltage_) { + this->cell_14_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); + } + if (this->cell_15_voltage_) { + this->cell_15_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); + } + break; + case 6: + if (this->cell_16_voltage_) { + this->cell_16_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); + } + break; + } + break; + default: break; } diff --git a/esphome/components/daly_bms/daly_bms.h b/esphome/components/daly_bms/daly_bms.h index b5d4c8ae39..d4fe84fe46 100644 --- a/esphome/components/daly_bms/daly_bms.h +++ b/esphome/components/daly_bms/daly_bms.h @@ -6,6 +6,8 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/uart/uart.h" +#include + namespace esphome { namespace daly_bms { @@ -37,6 +39,23 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice { void set_cells_number_sensor(sensor::Sensor *cells_number) { cells_number_ = cells_number; } void set_temperature_1_sensor(sensor::Sensor *temperature_1_sensor) { temperature_1_sensor_ = temperature_1_sensor; } void set_temperature_2_sensor(sensor::Sensor *temperature_2_sensor) { temperature_2_sensor_ = temperature_2_sensor; } + void set_cell_1_voltage_sensor(sensor::Sensor *cell_1_voltage) { cell_1_voltage_ = cell_1_voltage; } + void set_cell_2_voltage_sensor(sensor::Sensor *cell_2_voltage) { cell_2_voltage_ = cell_2_voltage; } + void set_cell_3_voltage_sensor(sensor::Sensor *cell_3_voltage) { cell_3_voltage_ = cell_3_voltage; } + void set_cell_4_voltage_sensor(sensor::Sensor *cell_4_voltage) { cell_4_voltage_ = cell_4_voltage; } + void set_cell_5_voltage_sensor(sensor::Sensor *cell_5_voltage) { cell_5_voltage_ = cell_5_voltage; } + void set_cell_6_voltage_sensor(sensor::Sensor *cell_6_voltage) { cell_6_voltage_ = cell_6_voltage; } + void set_cell_7_voltage_sensor(sensor::Sensor *cell_7_voltage) { cell_7_voltage_ = cell_7_voltage; } + void set_cell_8_voltage_sensor(sensor::Sensor *cell_8_voltage) { cell_8_voltage_ = cell_8_voltage; } + void set_cell_9_voltage_sensor(sensor::Sensor *cell_9_voltage) { cell_9_voltage_ = cell_9_voltage; } + void set_cell_10_voltage_sensor(sensor::Sensor *cell_10_voltage) { cell_10_voltage_ = cell_10_voltage; } + void set_cell_11_voltage_sensor(sensor::Sensor *cell_11_voltage) { cell_11_voltage_ = cell_11_voltage; } + void set_cell_12_voltage_sensor(sensor::Sensor *cell_12_voltage) { cell_12_voltage_ = cell_12_voltage; } + void set_cell_13_voltage_sensor(sensor::Sensor *cell_13_voltage) { cell_13_voltage_ = cell_13_voltage; } + void set_cell_14_voltage_sensor(sensor::Sensor *cell_14_voltage) { cell_14_voltage_ = cell_14_voltage; } + void set_cell_15_voltage_sensor(sensor::Sensor *cell_15_voltage) { cell_15_voltage_ = cell_15_voltage; } + void set_cell_16_voltage_sensor(sensor::Sensor *cell_16_voltage) { cell_16_voltage_ = cell_16_voltage; } + // TEXT_SENSORS void set_status_text_sensor(text_sensor::TextSensor *status_text_sensor) { status_text_sensor_ = status_text_sensor; } // BINARY_SENSORS @@ -52,11 +71,14 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice { void update() override; float get_setup_priority() const override; + void set_address(uint8_t address) { this->addr_ = address; } protected: void request_data_(uint8_t data_id); void decode_data_(std::vector data); + uint8_t addr_; + sensor::Sensor *voltage_sensor_{nullptr}; sensor::Sensor *current_sensor_{nullptr}; sensor::Sensor *battery_level_sensor_{nullptr}; @@ -72,6 +94,22 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice { sensor::Sensor *cells_number_{nullptr}; sensor::Sensor *temperature_1_sensor_{nullptr}; sensor::Sensor *temperature_2_sensor_{nullptr}; + sensor::Sensor *cell_1_voltage_{nullptr}; + sensor::Sensor *cell_2_voltage_{nullptr}; + sensor::Sensor *cell_3_voltage_{nullptr}; + sensor::Sensor *cell_4_voltage_{nullptr}; + sensor::Sensor *cell_5_voltage_{nullptr}; + sensor::Sensor *cell_6_voltage_{nullptr}; + sensor::Sensor *cell_7_voltage_{nullptr}; + sensor::Sensor *cell_8_voltage_{nullptr}; + sensor::Sensor *cell_9_voltage_{nullptr}; + sensor::Sensor *cell_10_voltage_{nullptr}; + sensor::Sensor *cell_11_voltage_{nullptr}; + sensor::Sensor *cell_12_voltage_{nullptr}; + sensor::Sensor *cell_13_voltage_{nullptr}; + sensor::Sensor *cell_14_voltage_{nullptr}; + sensor::Sensor *cell_15_voltage_{nullptr}; + sensor::Sensor *cell_16_voltage_{nullptr}; text_sensor::TextSensor *status_text_sensor_{nullptr}; diff --git a/esphome/components/daly_bms/sensor.py b/esphome/components/daly_bms/sensor.py index 1d0ee89914..2274a2153a 100644 --- a/esphome/components/daly_bms/sensor.py +++ b/esphome/components/daly_bms/sensor.py @@ -11,14 +11,11 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_BATTERY, DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, UNIT_VOLT, UNIT_AMPERE, UNIT_PERCENT, UNIT_CELSIUS, - UNIT_EMPTY, ICON_FLASH, ICON_PERCENT, ICON_COUNTER, @@ -39,6 +36,22 @@ CONF_REMAINING_CAPACITY = "remaining_capacity" CONF_TEMPERATURE_1 = "temperature_1" CONF_TEMPERATURE_2 = "temperature_2" +CONF_CELL_1_VOLTAGE = "cell_1_voltage" +CONF_CELL_2_VOLTAGE = "cell_2_voltage" +CONF_CELL_3_VOLTAGE = "cell_3_voltage" +CONF_CELL_4_VOLTAGE = "cell_4_voltage" +CONF_CELL_5_VOLTAGE = "cell_5_voltage" +CONF_CELL_6_VOLTAGE = "cell_6_voltage" +CONF_CELL_7_VOLTAGE = "cell_7_voltage" +CONF_CELL_8_VOLTAGE = "cell_8_voltage" +CONF_CELL_9_VOLTAGE = "cell_9_voltage" +CONF_CELL_10_VOLTAGE = "cell_10_voltage" +CONF_CELL_11_VOLTAGE = "cell_11_voltage" +CONF_CELL_12_VOLTAGE = "cell_12_voltage" +CONF_CELL_13_VOLTAGE = "cell_13_voltage" +CONF_CELL_14_VOLTAGE = "cell_14_voltage" +CONF_CELL_15_VOLTAGE = "cell_15_voltage" +CONF_CELL_16_VOLTAGE = "cell_16_voltage" ICON_CURRENT_DC = "mdi:current-dc" ICON_BATTERY_OUTLINE = "mdi:battery-outline" ICON_THERMOMETER_CHEVRON_UP = "mdi:thermometer-chevron-up" @@ -63,117 +76,142 @@ TYPES = [ CONF_REMAINING_CAPACITY, CONF_TEMPERATURE_1, CONF_TEMPERATURE_2, + CONF_CELL_1_VOLTAGE, + CONF_CELL_2_VOLTAGE, + CONF_CELL_3_VOLTAGE, + CONF_CELL_4_VOLTAGE, + CONF_CELL_5_VOLTAGE, + CONF_CELL_6_VOLTAGE, + CONF_CELL_7_VOLTAGE, + CONF_CELL_8_VOLTAGE, + CONF_CELL_9_VOLTAGE, + CONF_CELL_10_VOLTAGE, + CONF_CELL_11_VOLTAGE, + CONF_CELL_12_VOLTAGE, + CONF_CELL_13_VOLTAGE, + CONF_CELL_14_VOLTAGE, + CONF_CELL_15_VOLTAGE, + CONF_CELL_16_VOLTAGE, ] +CELL_VOLTAGE_SCHEMA = sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + icon=ICON_FLASH, + accuracy_decimals=3, +) + CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(CONF_BMS_DALY_ID): cv.use_id(DalyBmsComponent), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, - ICON_FLASH, - 1, - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_VOLT, + icon=ICON_FLASH, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, - ICON_CURRENT_DC, - 1, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + icon=ICON_CURRENT_DC, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_PERCENT, - 1, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MAX_CELL_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, - ICON_FLASH, - 2, - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_VOLT, + icon=ICON_FLASH, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MAX_CELL_VOLTAGE_NUMBER): sensor.sensor_schema( - UNIT_EMPTY, - ICON_COUNTER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + icon=ICON_COUNTER, + accuracy_decimals=0, ), cv.Optional(CONF_MIN_CELL_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, - ICON_FLASH, - 2, - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_VOLT, + icon=ICON_FLASH, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MIN_CELL_VOLTAGE_NUMBER): sensor.sensor_schema( - UNIT_EMPTY, - ICON_COUNTER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + icon=ICON_COUNTER, + accuracy_decimals=0, ), cv.Optional(CONF_MAX_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_THERMOMETER_CHEVRON_UP, - 0, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER_CHEVRON_UP, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MAX_TEMPERATURE_PROBE_NUMBER): sensor.sensor_schema( - UNIT_EMPTY, - ICON_COUNTER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + icon=ICON_COUNTER, + accuracy_decimals=0, ), cv.Optional(CONF_MIN_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_THERMOMETER_CHEVRON_DOWN, - 0, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER_CHEVRON_DOWN, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MIN_TEMPERATURE_PROBE_NUMBER): sensor.sensor_schema( - UNIT_EMPTY, - ICON_COUNTER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + icon=ICON_COUNTER, + accuracy_decimals=0, ), cv.Optional(CONF_REMAINING_CAPACITY): sensor.sensor_schema( - UNIT_AMPERE_HOUR, - ICON_GAUGE, - 2, - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE_HOUR, + icon=ICON_GAUGE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CELLS_NUMBER): sensor.sensor_schema( - UNIT_EMPTY, - ICON_COUNTER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + icon=ICON_COUNTER, + accuracy_decimals=0, ), cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_THERMOMETER, - 0, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_THERMOMETER, - 0, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_CELL_1_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_2_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_3_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_4_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_5_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_6_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_7_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_8_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_9_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_10_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_11_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_12_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_13_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_14_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_15_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_16_VOLTAGE): CELL_VOLTAGE_SCHEMA, } ).extend(cv.COMPONENT_SCHEMA) ) diff --git a/esphome/components/daly_bms/text_sensor.py b/esphome/components/daly_bms/text_sensor.py index de49a0b4b9..9f23e5f373 100644 --- a/esphome/components/daly_bms/text_sensor.py +++ b/esphome/components/daly_bms/text_sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor -from esphome.const import CONF_ICON, CONF_ID, CONF_STATUS +from esphome.const import CONF_STATUS from . import DalyBmsComponent, CONF_BMS_DALY_ID ICON_CAR_BATTERY = "mdi:car-battery" @@ -14,11 +14,8 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(CONF_BMS_DALY_ID): cv.use_id(DalyBmsComponent), - cv.Optional(CONF_STATUS): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - cv.Optional(CONF_ICON, default=ICON_CAR_BATTERY): cv.icon, - } + cv.Optional(CONF_STATUS): text_sensor.text_sensor_schema( + icon=ICON_CAR_BATTERY ), } ).extend(cv.COMPONENT_SCHEMA) @@ -28,8 +25,7 @@ CONFIG_SCHEMA = cv.All( async def setup_conf(config, key, hub): if key in config: conf = config[key] - sens = cg.new_Pvariable(conf[CONF_ID]) - await text_sensor.register_text_sensor(sens, conf) + sens = await text_sensor.new_text_sensor(conf) cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py index 6194a55205..e0994be6a0 100644 --- a/esphome/components/dashboard_import/__init__.py +++ b/esphome/components/dashboard_import/__init__.py @@ -1,12 +1,19 @@ +import base64 +import secrets from pathlib import Path +from typing import Optional + +import requests import esphome.codegen as cg import esphome.config_validation as cv +import esphome.final_validate as fv +from esphome import git from esphome.components.packages import validate_source_shorthand +from esphome.const import CONF_REF, CONF_WIFI, CONF_ESPHOME, CONF_PROJECT from esphome.wizard import wizard_file from esphome.yaml_util import dump - dashboard_import_ns = cg.esphome_ns.namespace("dashboard_import") # payload is in `esphomelib` mdns record, which only exists if api @@ -18,18 +25,45 @@ CODEOWNERS = ["@esphome/core"] def validate_import_url(value): value = cv.string_strict(value) value = cv.Length(max=255)(value) - # ignore result, only check if it's a valid shorthand validate_source_shorthand(value) return value +def validate_full_url(config): + if not config[CONF_IMPORT_FULL_CONFIG]: + return config + source = validate_source_shorthand(config[CONF_PACKAGE_IMPORT_URL]) + if CONF_REF not in source: + raise cv.Invalid( + "Must specify a ref (branch or tag) to import from when importing full config" + ) + return config + + CONF_PACKAGE_IMPORT_URL = "package_import_url" -CONFIG_SCHEMA = cv.Schema( - { - cv.Required(CONF_PACKAGE_IMPORT_URL): validate_import_url, - } +CONF_IMPORT_FULL_CONFIG = "import_full_config" + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_PACKAGE_IMPORT_URL): validate_import_url, + cv.Optional(CONF_IMPORT_FULL_CONFIG, default=False): cv.boolean, + } + ), + validate_full_url, ) + +def _final_validate(config): + full_config = fv.full_config.get()[CONF_ESPHOME] + if CONF_PROJECT not in full_config: + raise cv.Invalid( + "Dashboard import requires the `esphome` -> `project` information to be provided." + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate + WIFI_CONFIG = """ wifi: @@ -40,33 +74,95 @@ wifi: async def to_code(config): cg.add_define("USE_DASHBOARD_IMPORT") - cg.add(dashboard_import_ns.set_package_import_url(config[CONF_PACKAGE_IMPORT_URL])) + url = config[CONF_PACKAGE_IMPORT_URL] + if config[CONF_IMPORT_FULL_CONFIG]: + url += "?full_config" + cg.add(dashboard_import_ns.set_package_import_url(url)) -def import_config(path: str, name: str, project_name: str, import_url: str) -> None: +def import_config( + path: str, + name: str, + friendly_name: Optional[str], + project_name: str, + import_url: str, + network: str = CONF_WIFI, + encryption: bool = False, +) -> None: p = Path(path) if p.exists(): raise FileExistsError if project_name == "esphome.web": + if "esp32c3" in import_url: + board = "esp32-c3-devkitm-1" + platform = "ESP32" + elif "esp32s2" in import_url: + board = "esp32-s2-saola-1" + platform = "ESP32" + elif "esp32s3" in import_url: + board = "esp32-s3-devkitc-1" + platform = "ESP32" + elif "esp32" in import_url: + board = "esp32dev" + platform = "ESP32" + elif "esp8266" in import_url: + board = "esp01_1m" + platform = "ESP8266" + elif "pico-w" in import_url: + board = "pico-w" + platform = "RP2040" + + kwargs = { + "name": name, + "friendly_name": friendly_name, + "platform": platform, + "board": board, + "ssid": "!secret wifi_ssid", + "psk": "!secret wifi_password", + } + if encryption: + noise_psk = secrets.token_bytes(32) + key = base64.b64encode(noise_psk).decode() + kwargs["api_encryption_key"] = key + p.write_text( - wizard_file( - name=name, - platform="ESP32" if "esp32" in import_url else "ESP8266", - board="esp32dev" if "esp32" in import_url else "esp01_1m", - ssid="!secret wifi_ssid", - psk="!secret wifi_password", - ), + wizard_file(**kwargs), encoding="utf8", ) else: - config = { - "substitutions": {"name": name}, - "packages": {project_name: import_url}, - "esphome": {"name_add_mac_suffix": False}, - } - p.write_text( - dump(config) + WIFI_CONFIG, - encoding="utf8", - ) + git_file = git.GitFile.from_shorthand(import_url) + + if git_file.query and "full_config" in git_file.query: + url = git_file.raw_url + try: + req = requests.get(url, timeout=30) + req.raise_for_status() + except requests.exceptions.RequestException as e: + raise ValueError(f"Error while fetching {url}: {e}") from e + + p.write_text(req.text, encoding="utf8") + + else: + substitutions = {"name": name} + esphome_core = {"name": "${name}", "name_add_mac_suffix": False} + if friendly_name: + substitutions["friendly_name"] = friendly_name + esphome_core["friendly_name"] = "${friendly_name}" + config = { + "substitutions": substitutions, + "packages": {project_name: import_url}, + "esphome": esphome_core, + } + if encryption: + noise_psk = secrets.token_bytes(32) + key = base64.b64encode(noise_psk).decode() + config["api"] = {"encryption": {"key": key}} + + output = dump(config) + + if network == CONF_WIFI: + output += WIFI_CONFIG + + p.write_text(output, encoding="utf8") diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py index 98ad9e2b10..9742b3b19e 100644 --- a/esphome/components/debug/__init__.py +++ b/esphome/components/debug/__init__.py @@ -1,68 +1,47 @@ -import esphome.config_validation as cv import esphome.codegen as cg -from esphome.components import sensor, text_sensor +import esphome.config_validation as cv from esphome.const import ( - CONF_ID, - CONF_DEVICE, - CONF_FREE, - CONF_FRAGMENTATION, CONF_BLOCK, + CONF_DEVICE, + CONF_FRAGMENTATION, + CONF_FREE, + CONF_ID, CONF_LOOP_TIME, - UNIT_MILLISECOND, - UNIT_PERCENT, - UNIT_BYTES, - ICON_COUNTER, - ICON_TIMER, ) CODEOWNERS = ["@OttoWinter"] DEPENDENCIES = ["logger"] +CONF_DEBUG_ID = "debug_id" debug_ns = cg.esphome_ns.namespace("debug") DebugComponent = debug_ns.class_("DebugComponent", cg.PollingComponent) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(DebugComponent), - cv.Optional(CONF_DEVICE): text_sensor.TEXT_SENSOR_SCHEMA.extend( - {cv.GenerateID(): cv.declare_id(text_sensor.TextSensor)} - ), - cv.Optional(CONF_FREE): sensor.sensor_schema(UNIT_BYTES, ICON_COUNTER, 0), - cv.Optional(CONF_BLOCK): sensor.sensor_schema(UNIT_BYTES, ICON_COUNTER, 0), - cv.Optional(CONF_FRAGMENTATION): cv.All( - cv.only_on_esp8266, - cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)), - sensor.sensor_schema(UNIT_PERCENT, ICON_COUNTER, 1), - ), - cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema( - UNIT_MILLISECOND, ICON_TIMER, 1 - ), - } -).extend(cv.polling_component_schema("60s")) +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(DebugComponent), + cv.Optional(CONF_DEVICE): cv.invalid( + "The 'device' option has been moved to the 'debug' text_sensor component" + ), + cv.Optional(CONF_FREE): cv.invalid( + "The 'free' option has been moved to the 'debug' sensor component" + ), + cv.Optional(CONF_BLOCK): cv.invalid( + "The 'block' option has been moved to the 'debug' sensor component" + ), + cv.Optional(CONF_FRAGMENTATION): cv.invalid( + "The 'fragmentation' option has been moved to the 'debug' sensor component" + ), + cv.Optional(CONF_LOOP_TIME): cv.invalid( + "The 'loop_time' option has been moved to the 'debug' sensor component" + ), + } + ).extend(cv.polling_component_schema("60s")), + cv.only_on(["esp32", "esp8266"]), +) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - - if CONF_DEVICE in config: - sens = cg.new_Pvariable(config[CONF_DEVICE][CONF_ID]) - await text_sensor.register_text_sensor(sens, config[CONF_DEVICE]) - cg.add(var.set_device_info_sensor(sens)) - - if CONF_FREE in config: - sens = await sensor.new_sensor(config[CONF_FREE]) - cg.add(var.set_free_sensor(sens)) - - if CONF_BLOCK in config: - sens = await sensor.new_sensor(config[CONF_BLOCK]) - cg.add(var.set_block_sensor(sens)) - - if CONF_FRAGMENTATION in config: - sens = await sensor.new_sensor(config[CONF_FRAGMENTATION]) - cg.add(var.set_fragmentation_sensor(sens)) - - if CONF_LOOP_TIME in config: - sens = await sensor.new_sensor(config[CONF_LOOP_TIME]) - cg.add(var.set_loop_time_sensor(sens)) diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index 41bf5f50c7..9843fa1c99 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -37,22 +37,25 @@ static uint32_t get_free_heap() { } void DebugComponent::dump_config() { +#ifndef ESPHOME_LOG_HAS_DEBUG + return; // Can't log below if debug logging is disabled +#endif + std::string device_info; + std::string reset_reason; device_info.reserve(256); -#ifndef ESPHOME_LOG_HAS_DEBUG - ESP_LOGE(TAG, "Debug Component requires debug log level!"); - this->status_set_error(); - return; -#endif - ESP_LOGCONFIG(TAG, "Debug component:"); +#ifdef USE_TEXT_SENSOR LOG_TEXT_SENSOR(" ", "Device info", this->device_info_); +#endif // USE_TEXT_SENSOR +#ifdef USE_SENSOR LOG_SENSOR(" ", "Free space on heap", this->free_sensor_); LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_); -#if defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_); -#endif +#endif // defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#endif // USE_SENSOR ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION); device_info += ESPHOME_VERSION; @@ -142,7 +145,6 @@ void DebugComponent::dump_config() { device_info += "|EFuse MAC: "; device_info += mac; - const char *reset_reason; switch (rtc_get_reset_reason(0)) { case POWERON_RESET: reset_reason = "Power On Reset"; @@ -192,7 +194,7 @@ void DebugComponent::dump_config() { default: reset_reason = "Unknown Reset Reason"; } - ESP_LOGD(TAG, "Reset Reason: %s", reset_reason); + ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str()); device_info += "|Reset: "; device_info += reset_reason; @@ -266,13 +268,20 @@ void DebugComponent::dump_config() { device_info += ESP.getResetReason().c_str(); device_info += "|"; device_info += ESP.getResetInfo().c_str(); + + reset_reason = ESP.getResetReason().c_str(); #endif +#ifdef USE_TEXT_SENSOR if (this->device_info_ != nullptr) { if (device_info.length() > 255) device_info.resize(255); this->device_info_->publish_state(device_info); } + if (this->reset_reason_ != nullptr) { + this->reset_reason_->publish_state(reset_reason); + } +#endif // USE_TEXT_SENSOR } void DebugComponent::loop() { @@ -284,6 +293,7 @@ void DebugComponent::loop() { this->status_momentary_warning("heap", 1000); } +#ifdef USE_SENSOR // calculate loop time - from last call to this one if (this->loop_time_sensor_ != nullptr) { uint32_t now = millis(); @@ -291,9 +301,11 @@ void DebugComponent::loop() { this->max_loop_time_ = std::max(this->max_loop_time_, loop_time); this->last_loop_timetag_ = now; } +#endif // USE_SENSOR } void DebugComponent::update() { +#ifdef USE_SENSOR if (this->free_sensor_ != nullptr) { this->free_sensor_->publish_state(get_free_heap()); } @@ -307,7 +319,7 @@ void DebugComponent::update() { #endif } -#if defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) if (this->fragmentation_sensor_ != nullptr) { // NOLINTNEXTLINE(readability-static-accessed-through-instance) this->fragmentation_sensor_->publish_state(ESP.getHeapFragmentation()); @@ -318,6 +330,7 @@ void DebugComponent::update() { this->loop_time_sensor_->publish_state(this->max_loop_time_); this->max_loop_time_ = 0; } +#endif // USE_SENSOR } float DebugComponent::get_setup_priority() const { return setup_priority::LATE; } diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index a362c52617..b80fda55eb 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -1,10 +1,16 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/macros.h" #include "esphome/core/helpers.h" -#include "esphome/components/text_sensor/text_sensor.h" + +#ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif namespace esphome { namespace debug { @@ -16,27 +22,37 @@ class DebugComponent : public PollingComponent { float get_setup_priority() const override; void dump_config() override; +#ifdef USE_TEXT_SENSOR void set_device_info_sensor(text_sensor::TextSensor *device_info) { device_info_ = device_info; } + void set_reset_reason_sensor(text_sensor::TextSensor *reset_reason) { reset_reason_ = reset_reason; } +#endif // USE_TEXT_SENSOR +#ifdef USE_SENSOR void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; } void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; } -#if defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; } #endif void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; } - +#endif // USE_SENSOR protected: uint32_t free_heap_{}; +#ifdef USE_SENSOR uint32_t last_loop_timetag_{0}; uint32_t max_loop_time_{0}; - text_sensor::TextSensor *device_info_{nullptr}; sensor::Sensor *free_sensor_{nullptr}; sensor::Sensor *block_sensor_{nullptr}; -#if defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) sensor::Sensor *fragmentation_sensor_{nullptr}; #endif sensor::Sensor *loop_time_sensor_{nullptr}; +#endif // USE_SENSOR + +#ifdef USE_TEXT_SENSOR + text_sensor::TextSensor *device_info_{nullptr}; + text_sensor::TextSensor *reset_reason_{nullptr}; +#endif // USE_TEXT_SENSOR }; } // namespace debug diff --git a/esphome/components/debug/sensor.py b/esphome/components/debug/sensor.py new file mode 100644 index 0000000000..f7ea07d138 --- /dev/null +++ b/esphome/components/debug/sensor.py @@ -0,0 +1,70 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_FREE, + CONF_FRAGMENTATION, + CONF_BLOCK, + CONF_LOOP_TIME, + ENTITY_CATEGORY_DIAGNOSTIC, + UNIT_MILLISECOND, + UNIT_PERCENT, + UNIT_BYTES, + ICON_COUNTER, + ICON_TIMER, +) +from . import CONF_DEBUG_ID, DebugComponent + +DEPENDENCIES = ["debug"] + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_DEBUG_ID): cv.use_id(DebugComponent), + cv.Optional(CONF_FREE): sensor.sensor_schema( + unit_of_measurement=UNIT_BYTES, + icon=ICON_COUNTER, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_BLOCK): sensor.sensor_schema( + unit_of_measurement=UNIT_BYTES, + icon=ICON_COUNTER, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_FRAGMENTATION): cv.All( + cv.only_on_esp8266, + cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)), + sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_COUNTER, + accuracy_decimals=1, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + ), + cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLISECOND, + icon=ICON_TIMER, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), +} + + +async def to_code(config): + debug_component = await cg.get_variable(config[CONF_DEBUG_ID]) + + if CONF_FREE in config: + sens = await sensor.new_sensor(config[CONF_FREE]) + cg.add(debug_component.set_free_sensor(sens)) + + if CONF_BLOCK in config: + sens = await sensor.new_sensor(config[CONF_BLOCK]) + cg.add(debug_component.set_block_sensor(sens)) + + if CONF_FRAGMENTATION in config: + sens = await sensor.new_sensor(config[CONF_FRAGMENTATION]) + cg.add(debug_component.set_fragmentation_sensor(sens)) + + if CONF_LOOP_TIME in config: + sens = await sensor.new_sensor(config[CONF_LOOP_TIME]) + cg.add(debug_component.set_loop_time_sensor(sens)) diff --git a/esphome/components/debug/text_sensor.py b/esphome/components/debug/text_sensor.py new file mode 100644 index 0000000000..24f938a0e2 --- /dev/null +++ b/esphome/components/debug/text_sensor.py @@ -0,0 +1,40 @@ +from esphome.components import text_sensor +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import ( + CONF_DEVICE, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_CHIP, + ICON_RESTART, +) + +from . import CONF_DEBUG_ID, DebugComponent + +DEPENDENCIES = ["debug"] + + +CONF_RESET_REASON = "reset_reason" +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_DEBUG_ID): cv.use_id(DebugComponent), + cv.Optional(CONF_DEVICE): text_sensor.text_sensor_schema( + icon=ICON_CHIP, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_RESET_REASON): text_sensor.text_sensor_schema( + icon=ICON_RESTART, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + debug_component = await cg.get_variable(config[CONF_DEBUG_ID]) + + if CONF_DEVICE in config: + sens = await text_sensor.new_text_sensor(config[CONF_DEVICE]) + cg.add(debug_component.set_device_info_sensor(sens)) + if CONF_RESET_REASON in config: + sens = await text_sensor.new_text_sensor(config[CONF_RESET_REASON]) + cg.add(debug_component.set_reset_reason_sensor(sens)) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index ba4c2c0d7e..bbd10d58c5 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -1,19 +1,104 @@ import esphome.codegen as cg +from esphome.components import time import esphome.config_validation as cv from esphome import pins, automation from esphome.const import ( + CONF_HOUR, CONF_ID, + CONF_MINUTE, CONF_MODE, CONF_NUMBER, CONF_PINS, CONF_RUN_DURATION, + CONF_SECOND, CONF_SLEEP_DURATION, + CONF_TIME_ID, CONF_WAKEUP_PIN, ) +from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32C3, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) + +WAKEUP_PINS = { + VARIANT_ESP32: [ + 0, + 2, + 4, + 12, + 13, + 14, + 15, + 25, + 26, + 27, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + ], + VARIANT_ESP32C3: [0, 1, 2, 3, 4, 5], + VARIANT_ESP32S2: [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + ], + VARIANT_ESP32S3: [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + ], +} + def validate_pin_number(value): - valid_pins = [0, 2, 4, 12, 13, 14, 15, 25, 26, 27, 32, 33, 34, 35, 36, 37, 38, 39] + valid_pins = WAKEUP_PINS.get(get_esp32_variant(), WAKEUP_PINS[VARIANT_ESP32]) if value[CONF_NUMBER] not in valid_pins: raise cv.Invalid( f"Only pins {', '.join(str(x) for x in valid_pins)} support wakeup" @@ -21,11 +106,26 @@ def validate_pin_number(value): return value +def validate_config(config): + if get_esp32_variant() == VARIANT_ESP32C3 and CONF_ESP32_EXT1_WAKEUP in config: + raise cv.Invalid("ESP32-C3 does not support wakeup from touch.") + if get_esp32_variant() == VARIANT_ESP32C3 and CONF_TOUCH_WAKEUP in config: + raise cv.Invalid("ESP32-C3 does not support wakeup from ext1") + return config + + deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep") DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component) EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action) PreventDeepSleepAction = deep_sleep_ns.class_( - "PreventDeepSleepAction", automation.Action + "PreventDeepSleepAction", + automation.Action, + cg.Parented.template(DeepSleepComponent), +) +AllowDeepSleepAction = deep_sleep_ns.class_( + "AllowDeepSleepAction", + automation.Action, + cg.Parented.template(DeepSleepComponent), ) WakeupPinMode = deep_sleep_ns.enum("WakeupPinMode") @@ -49,6 +149,7 @@ CONF_TOUCH_WAKEUP = "touch_wakeup" CONF_DEFAULT = "default" CONF_GPIO_WAKEUP_REASON = "gpio_wakeup_reason" CONF_TOUCH_WAKEUP_REASON = "touch_wakeup_reason" +CONF_UNTIL = "until" WAKEUP_CAUSES_SCHEMA = cv.Schema( { @@ -139,20 +240,30 @@ async def to_code(config): cg.add_define("USE_DEEP_SLEEP") -DEEP_SLEEP_ENTER_SCHEMA = automation.maybe_simple_id( +DEEP_SLEEP_ACTION_SCHEMA = cv.Schema( { cv.GenerateID(): cv.use_id(DeepSleepComponent), - cv.Optional(CONF_SLEEP_DURATION): cv.templatable( - cv.positive_time_period_milliseconds - ), } ) - -DEEP_SLEEP_PREVENT_SCHEMA = automation.maybe_simple_id( - { - cv.GenerateID(): cv.use_id(DeepSleepComponent), - } +DEEP_SLEEP_ENTER_SCHEMA = cv.All( + automation.maybe_simple_id( + DEEP_SLEEP_ACTION_SCHEMA.extend( + cv.Schema( + { + cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable( + cv.positive_time_period_milliseconds + ), + # Only on ESP32 due to how long the RTC on ESP8266 can stay asleep + cv.Exclusive(CONF_UNTIL, "time"): cv.All( + cv.only_on_esp32, cv.time_of_day + ), + cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + } + ) + ) + ), + cv.has_none_or_all_keys(CONF_UNTIL, CONF_TIME_ID), ) @@ -165,12 +276,28 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args): if CONF_SLEEP_DURATION in config: template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.int32) cg.add(var.set_sleep_duration(template_)) + + if CONF_UNTIL in config: + until = config[CONF_UNTIL] + cg.add(var.set_until(until[CONF_HOUR], until[CONF_MINUTE], until[CONF_SECOND])) + + time_ = await cg.get_variable(config[CONF_TIME_ID]) + cg.add(var.set_time(time_)) + return var @automation.register_action( - "deep_sleep.prevent", PreventDeepSleepAction, DEEP_SLEEP_PREVENT_SCHEMA + "deep_sleep.prevent", + PreventDeepSleepAction, + automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA), ) -async def deep_sleep_prevent_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) +@automation.register_action( + "deep_sleep.allow", + AllowDeepSleepAction, + automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA), +) +async def deep_sleep_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 7774014d3d..f6472a123c 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -1,6 +1,7 @@ #include "deep_sleep_component.h" -#include "esphome/core/log.h" +#include #include "esphome/core/application.h" +#include "esphome/core/log.h" #ifdef USE_ESP8266 #include @@ -20,6 +21,7 @@ optional DeepSleepComponent::get_run_duration_() const { switch (wakeup_cause) { case ESP_SLEEP_WAKEUP_EXT0: case ESP_SLEEP_WAKEUP_EXT1: + case ESP_SLEEP_WAKEUP_GPIO: return this->wakeup_cause_to_run_duration_->gpio_cause; case ESP_SLEEP_WAKEUP_TOUCHPAD: return this->wakeup_cause_to_run_duration_->touch_cause; @@ -71,16 +73,27 @@ float DeepSleepComponent::get_loop_priority() const { return -100.0f; // run after everything else is ready } void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; } -#ifdef USE_ESP32 +#if defined(USE_ESP32) void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) { this->wakeup_pin_mode_ = wakeup_pin_mode; } +#endif + +#if defined(USE_ESP32) +#if !defined(USE_ESP32_VARIANT_ESP32C3) + void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; } + void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; } + +#endif + void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) { wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration; } + #endif + void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; } void DeepSleepComponent::begin_sleep(bool manual) { if (this->prevent_ && !manual) { @@ -101,10 +114,13 @@ void DeepSleepComponent::begin_sleep(bool manual) { #endif ESP_LOGI(TAG, "Beginning Deep Sleep"); - + if (this->sleep_duration_.has_value()) { + ESP_LOGI(TAG, "Sleeping for %" PRId64 "us", *this->sleep_duration_); + } App.run_safe_shutdown_hooks(); -#ifdef USE_ESP32 +#if defined(USE_ESP32) +#if !defined(USE_ESP32_VARIANT_ESP32C3) if (this->sleep_duration_.has_value()) esp_sleep_enable_timer_wakeup(*this->sleep_duration_); if (this->wakeup_pin_ != nullptr) { @@ -122,7 +138,19 @@ void DeepSleepComponent::begin_sleep(bool manual) { esp_sleep_enable_touchpad_wakeup(); esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); } - +#endif +#ifdef USE_ESP32_VARIANT_ESP32C3 + if (this->sleep_duration_.has_value()) + esp_sleep_enable_timer_wakeup(*this->sleep_duration_); + if (this->wakeup_pin_ != nullptr) { + bool level = !this->wakeup_pin_->is_inverted(); + if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { + level = !level; + } + esp_deep_sleep_enable_gpio_wakeup(1 << this->wakeup_pin_->get_pin(), + static_cast(level)); + } +#endif esp_deep_sleep_start(); #endif @@ -132,6 +160,7 @@ void DeepSleepComponent::begin_sleep(bool manual) { } float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; } void DeepSleepComponent::prevent_deep_sleep() { this->prevent_ = true; } +void DeepSleepComponent::allow_deep_sleep() { this->prevent_ = false; } } // namespace deep_sleep } // namespace esphome diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 59df199a9f..2e54e53c56 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -1,14 +1,19 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/helpers.h" #include "esphome/core/automation.h" +#include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #ifdef USE_ESP32 #include #endif +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#include "esphome/core/time.h" +#endif + namespace esphome { namespace deep_sleep { @@ -57,23 +62,28 @@ class DeepSleepComponent : public Component { public: /// Set the duration in ms the component should sleep once it's in deep sleep mode. void set_sleep_duration(uint32_t time_ms); -#ifdef USE_ESP32 +#if defined(USE_ESP32) /** Set the pin to wake up to on the ESP32 once it's in deep sleep mode. * Use the inverted property to set the wakeup level. */ void set_wakeup_pin(InternalGPIOPin *pin) { this->wakeup_pin_ = pin; } void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode); +#endif + +#if defined(USE_ESP32) +#if !defined(USE_ESP32_VARIANT_ESP32C3) void set_ext1_wakeup(Ext1Wakeup ext1_wakeup); void set_touch_wakeup(bool touch_wakeup); +#endif // Set the duration in ms for how long the code should run before entering // deep sleep mode, according to the cause the ESP32 has woken. void set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration); - #endif + /// Set a duration in ms for how long the code should run before entering deep sleep mode. void set_run_duration(uint32_t time_ms); @@ -87,6 +97,7 @@ class DeepSleepComponent : public Component { void begin_sleep(bool manual = false); void prevent_deep_sleep(); + void allow_deep_sleep(); protected: // Returns nullopt if no run duration is set. Otherwise, returns the run @@ -113,25 +124,81 @@ template class EnterDeepSleepAction : public Action { EnterDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {} TEMPLATABLE_VALUE(uint32_t, sleep_duration); +#ifdef USE_TIME + void set_until(uint8_t hour, uint8_t minute, uint8_t second) { + this->hour_ = hour; + this->minute_ = minute; + this->second_ = second; + } + + void set_time(time::RealTimeClock *time) { this->time_ = time; } +#endif + void play(Ts... x) override { if (this->sleep_duration_.has_value()) { this->deep_sleep_->set_sleep_duration(this->sleep_duration_.value(x...)); } +#ifdef USE_TIME + + if (this->hour_.has_value()) { + auto time = this->time_->now(); + const uint32_t timestamp_now = time.timestamp; + + bool after_time = false; + if (time.hour > this->hour_) { + after_time = true; + } else { + if (time.hour == this->hour_) { + if (time.minute > this->minute_) { + after_time = true; + } else { + if (time.minute == this->minute_) { + if (time.second > this->second_) { + after_time = true; + } + } + } + } + } + + time.hour = *this->hour_; + time.minute = *this->minute_; + time.second = *this->second_; + time.recalc_timestamp_utc(); + + time_t timestamp = time.timestamp; // timestamp in local time zone + + if (after_time) + timestamp += 60 * 60 * 24; + + int32_t offset = ESPTime::timezone_offset(); + timestamp -= offset; // Change timestamp to utc + const uint32_t ms_left = (timestamp - timestamp_now) * 1000; + this->deep_sleep_->set_sleep_duration(ms_left); + } +#endif this->deep_sleep_->begin_sleep(true); } protected: DeepSleepComponent *deep_sleep_; +#ifdef USE_TIME + optional hour_; + optional minute_; + optional second_; + + time::RealTimeClock *time_; +#endif }; -template class PreventDeepSleepAction : public Action { +template class PreventDeepSleepAction : public Action, public Parented { public: - PreventDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {} + void play(Ts... x) override { this->parent_->prevent_deep_sleep(); } +}; - void play(Ts... x) override { this->deep_sleep_->prevent_deep_sleep(); } - - protected: - DeepSleepComponent *deep_sleep_; +template class AllowDeepSleepAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->allow_deep_sleep(); } }; } // namespace deep_sleep diff --git a/esphome/components/delonghi/__init__.py b/esphome/components/delonghi/__init__.py new file mode 100644 index 0000000000..0a81eb2da7 --- /dev/null +++ b/esphome/components/delonghi/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@grob6000"] diff --git a/esphome/components/delonghi/climate.py b/esphome/components/delonghi/climate.py new file mode 100644 index 0000000000..614706defe --- /dev/null +++ b/esphome/components/delonghi/climate.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ["climate_ir"] + +delonghi_ns = cg.esphome_ns.namespace("delonghi") +DelonghiClimate = delonghi_ns.class_("DelonghiClimate", climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DelonghiClimate), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/delonghi/delonghi.cpp b/esphome/components/delonghi/delonghi.cpp new file mode 100644 index 0000000000..9bc0b5753d --- /dev/null +++ b/esphome/components/delonghi/delonghi.cpp @@ -0,0 +1,186 @@ +#include "delonghi.h" +#include "esphome/components/remote_base/remote_base.h" + +namespace esphome { +namespace delonghi { + +static const char *const TAG = "delonghi.climate"; + +void DelonghiClimate::transmit_state() { + uint8_t remote_state[DELONGHI_STATE_FRAME_SIZE] = {0}; + remote_state[0] = DELONGHI_ADDRESS; + remote_state[1] = this->temperature_(); + remote_state[1] |= (this->fan_speed_()) << 5; + remote_state[2] = this->operation_mode_(); + // Calculate checksum + for (int i = 0; i < DELONGHI_STATE_FRAME_SIZE - 1; i++) { + remote_state[DELONGHI_STATE_FRAME_SIZE - 1] += remote_state[i]; + } + + auto transmit = this->transmitter_->transmit(); + auto *data = transmit.get_data(); + data->set_carrier_frequency(DELONGHI_IR_FREQUENCY); + + data->mark(DELONGHI_HEADER_MARK); + data->space(DELONGHI_HEADER_SPACE); + for (unsigned char b : remote_state) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(DELONGHI_BIT_MARK); + bool bit = b & mask; + data->space(bit ? DELONGHI_ONE_SPACE : DELONGHI_ZERO_SPACE); + } + } + data->mark(DELONGHI_BIT_MARK); + data->space(0); + + transmit.perform(); +} + +uint8_t DelonghiClimate::operation_mode_() { + uint8_t operating_mode = DELONGHI_MODE_ON; + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + operating_mode |= DELONGHI_MODE_COOL; + break; + case climate::CLIMATE_MODE_DRY: + operating_mode |= DELONGHI_MODE_DRY; + break; + case climate::CLIMATE_MODE_HEAT: + operating_mode |= DELONGHI_MODE_HEAT; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + operating_mode |= DELONGHI_MODE_AUTO; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + operating_mode |= DELONGHI_MODE_FAN; + break; + case climate::CLIMATE_MODE_OFF: + default: + operating_mode = DELONGHI_MODE_OFF; + break; + } + return operating_mode; +} + +uint16_t DelonghiClimate::fan_speed_() { + uint16_t fan_speed; + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + fan_speed = DELONGHI_FAN_LOW; + break; + case climate::CLIMATE_FAN_MEDIUM: + fan_speed = DELONGHI_FAN_MEDIUM; + break; + case climate::CLIMATE_FAN_HIGH: + fan_speed = DELONGHI_FAN_HIGH; + break; + case climate::CLIMATE_FAN_AUTO: + default: + fan_speed = DELONGHI_FAN_AUTO; + } + return fan_speed; +} + +uint8_t DelonghiClimate::temperature_() { + // Force special temperatures depending on the mode + uint8_t temperature = 0b0001; + switch (this->mode) { + case climate::CLIMATE_MODE_HEAT: + temperature = (uint8_t) roundf(this->target_temperature) - DELONGHI_TEMP_OFFSET_HEAT; + break; + case climate::CLIMATE_MODE_COOL: + case climate::CLIMATE_MODE_DRY: + case climate::CLIMATE_MODE_HEAT_COOL: + case climate::CLIMATE_MODE_FAN_ONLY: + case climate::CLIMATE_MODE_OFF: + default: + temperature = (uint8_t) roundf(this->target_temperature) - DELONGHI_TEMP_OFFSET_COOL; + } + if (temperature > 0x0F) { + temperature = 0x0F; // clamp maximum + } + return temperature; +} + +bool DelonghiClimate::parse_state_frame_(const uint8_t frame[]) { + uint8_t checksum = 0; + for (int i = 0; i < (DELONGHI_STATE_FRAME_SIZE - 1); i++) { + checksum += frame[i]; + } + if (frame[DELONGHI_STATE_FRAME_SIZE - 1] != checksum) { + return false; + } + uint8_t mode = frame[2] & 0x0F; + if (mode & DELONGHI_MODE_ON) { + switch (mode & 0x0E) { + case DELONGHI_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case DELONGHI_MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case DELONGHI_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case DELONGHI_MODE_AUTO: + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + break; + case DELONGHI_MODE_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + } + } else { + this->mode = climate::CLIMATE_MODE_OFF; + } + uint8_t temperature = frame[1] & 0x0F; + if (this->mode == climate::CLIMATE_MODE_HEAT) { + this->target_temperature = temperature + DELONGHI_TEMP_OFFSET_HEAT; + } else { + this->target_temperature = temperature + DELONGHI_TEMP_OFFSET_COOL; + } + uint8_t fan_mode = frame[1] >> 5; + switch (fan_mode) { + case DELONGHI_FAN_LOW: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case DELONGHI_FAN_MEDIUM: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case DELONGHI_FAN_HIGH: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case DELONGHI_FAN_AUTO: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + this->publish_state(); + return true; +} + +bool DelonghiClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t state_frame[DELONGHI_STATE_FRAME_SIZE] = {}; + if (!data.expect_item(DELONGHI_HEADER_MARK, DELONGHI_HEADER_SPACE)) { + return false; + } + for (uint8_t pos = 0; pos < DELONGHI_STATE_FRAME_SIZE; pos++) { + uint8_t byte = 0; + for (int8_t bit = 0; bit < 8; bit++) { + if (data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ONE_SPACE)) { + byte |= 1 << bit; + } else if (!data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ZERO_SPACE)) { + return false; + } + } + state_frame[pos] = byte; + if (pos == 0) { + // frame header + if (byte != DELONGHI_ADDRESS) { + return false; + } + } + } + return this->parse_state_frame_(state_frame); +} + +} // namespace delonghi +} // namespace esphome diff --git a/esphome/components/delonghi/delonghi.h b/esphome/components/delonghi/delonghi.h new file mode 100644 index 0000000000..d310a58aee --- /dev/null +++ b/esphome/components/delonghi/delonghi.h @@ -0,0 +1,64 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace delonghi { + +// Values for DELONGHI ARC43XXX IR Controllers +const uint8_t DELONGHI_ADDRESS = 83; + +// Temperature +const uint8_t DELONGHI_TEMP_MIN = 13; // Celsius +const uint8_t DELONGHI_TEMP_MAX = 32; // Celsius +const uint8_t DELONGHI_TEMP_OFFSET_COOL = 17; // Celsius +const uint8_t DELONGHI_TEMP_OFFSET_HEAT = 12; // Celsius + +// Modes +const uint8_t DELONGHI_MODE_AUTO = 0b1000; +const uint8_t DELONGHI_MODE_COOL = 0b0000; +const uint8_t DELONGHI_MODE_HEAT = 0b0110; +const uint8_t DELONGHI_MODE_DRY = 0b0010; +const uint8_t DELONGHI_MODE_FAN = 0b0100; +const uint8_t DELONGHI_MODE_OFF = 0b0000; +const uint8_t DELONGHI_MODE_ON = 0b0001; + +// Fan Speed +const uint8_t DELONGHI_FAN_AUTO = 0b00; +const uint8_t DELONGHI_FAN_HIGH = 0b01; +const uint8_t DELONGHI_FAN_MEDIUM = 0b10; +const uint8_t DELONGHI_FAN_LOW = 0b11; + +// IR Transmission - similar to NEC1 +const uint32_t DELONGHI_IR_FREQUENCY = 38000; +const uint32_t DELONGHI_HEADER_MARK = 9000; +const uint32_t DELONGHI_HEADER_SPACE = 4500; +const uint32_t DELONGHI_BIT_MARK = 465; +const uint32_t DELONGHI_ONE_SPACE = 1750; +const uint32_t DELONGHI_ZERO_SPACE = 670; + +// State Frame size +const uint8_t DELONGHI_STATE_FRAME_SIZE = 8; + +class DelonghiClimate : public climate_ir::ClimateIR { + public: + DelonghiClimate() + : climate_ir::ClimateIR(DELONGHI_TEMP_MIN, DELONGHI_TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, + climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {} + + protected: + // Transmit via IR the state of this climate controller. + void transmit_state() override; + uint8_t operation_mode_(); + uint16_t fan_speed_(); + uint8_t temperature_(); + // Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + bool parse_state_frame_(const uint8_t frame[]); +}; + +} // namespace delonghi +} // namespace esphome diff --git a/esphome/components/demo/__init__.py b/esphome/components/demo/__init__.py index 6b4a55aac9..05160bf8cb 100644 --- a/esphome/components/demo/__init__.py +++ b/esphome/components/demo/__init__.py @@ -37,12 +37,10 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, ICON_BLUETOOTH, ICON_BLUR, - ICON_EMPTY, ICON_THERMOMETER, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, UNIT_CELSIUS, - UNIT_EMPTY, UNIT_PERCENT, UNIT_WATT_HOURS, ) @@ -134,12 +132,8 @@ CONFIG_SCHEMA = cv.Schema( }, ], ): [ - binary_sensor.BINARY_SENSOR_SCHEMA.extend( + binary_sensor.binary_sensor_schema(DemoBinarySensor).extend( cv.polling_component_schema("60s") - ).extend( - { - cv.GenerateID(): cv.declare_id(DemoBinarySensor), - } ) ], cv.Optional( @@ -290,9 +284,10 @@ CONFIG_SCHEMA = cv.Schema( }, ], ): [ - number.NUMBER_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + number.number_schema(DemoNumber) + .extend(cv.COMPONENT_SCHEMA) + .extend( { - cv.GenerateID(): cv.declare_id(DemoNumber), cv.Required(CONF_TYPE): cv.enum(NUMBER_TYPES, int=True), cv.Required(CONF_MIN_VALUE): cv.float_, cv.Required(CONF_MAX_VALUE): cv.float_, @@ -339,12 +334,8 @@ CONFIG_SCHEMA = cv.Schema( }, ], ): [ - sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 0) - .extend(cv.polling_component_schema("60s")) - .extend( - { - cv.GenerateID(): cv.declare_id(DemoSensor), - } + sensor.sensor_schema(DemoSensor, accuracy_decimals=0).extend( + cv.polling_component_schema("60s") ) ], cv.Optional( @@ -359,13 +350,7 @@ CONFIG_SCHEMA = cv.Schema( CONF_ICON: ICON_BLUETOOTH, }, ], - ): [ - switch.SWITCH_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( - { - cv.GenerateID(): cv.declare_id(DemoSwitch), - } - ) - ], + ): [switch.switch_schema(DemoSwitch).extend(cv.COMPONENT_SCHEMA)], cv.Optional( CONF_TEXT_SENSORS, default=[ @@ -378,12 +363,8 @@ CONFIG_SCHEMA = cv.Schema( }, ], ): [ - text_sensor.TEXT_SENSOR_SCHEMA.extend( + text_sensor.text_sensor_schema(DemoTextSensor).extend( cv.polling_component_schema("60s") - ).extend( - { - cv.GenerateID(): cv.declare_id(DemoTextSensor), - } ) ], } @@ -392,9 +373,8 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): for conf in config[CONF_BINARY_SENSORS]: - var = cg.new_Pvariable(conf[CONF_ID]) + var = await binary_sensor.new_binary_sensor(conf) await cg.register_component(var, conf) - await binary_sensor.register_binary_sensor(var, conf) for conf in config[CONF_CLIMATES]: var = cg.new_Pvariable(conf[CONF_ID]) @@ -433,16 +413,13 @@ async def to_code(config): cg.add(var.set_type(conf[CONF_TYPE])) for conf in config[CONF_SENSORS]: - var = cg.new_Pvariable(conf[CONF_ID]) + var = await sensor.new_sensor(conf) await cg.register_component(var, conf) - await sensor.register_sensor(var, conf) for conf in config[CONF_SWITCHES]: - var = cg.new_Pvariable(conf[CONF_ID]) + var = await switch.new_switch(conf) await cg.register_component(var, conf) - await switch.register_switch(var, conf) for conf in config[CONF_TEXT_SENSORS]: - var = cg.new_Pvariable(conf[CONF_ID]) + var = await text_sensor.new_text_sensor(conf) await cg.register_component(var, conf) - await text_sensor.register_text_sensor(var, conf) diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h index 0cf48dd4ee..1ba80aabf5 100644 --- a/esphome/components/demo/demo_climate.h +++ b/esphome/components/demo/demo_climate.h @@ -111,6 +111,7 @@ class DemoClimate : public climate::Climate, public Component { climate::CLIMATE_FAN_MIDDLE, climate::CLIMATE_FAN_FOCUS, climate::CLIMATE_FAN_DIFFUSE, + climate::CLIMATE_FAN_QUIET, }); traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"}); traits.set_supported_swing_modes({ diff --git a/esphome/components/demo/demo_cover.h b/esphome/components/demo/demo_cover.h index ab039736fb..ec266d46ab 100644 --- a/esphome/components/demo/demo_cover.h +++ b/esphome/components/demo/demo_cover.h @@ -72,6 +72,7 @@ class DemoCover : public cover::Cover, public Component { traits.set_supports_tilt(true); break; case DemoCoverType::TYPE_4: + traits.set_supports_stop(true); traits.set_is_assumed_state(true); traits.set_supports_tilt(true); break; diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index 3cdfc8ab85..5ea04b4804 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -40,6 +40,7 @@ DEVICE = { NextAction = dfplayer_ns.class_("NextAction", automation.Action) PreviousAction = dfplayer_ns.class_("PreviousAction", automation.Action) +PlayMp3Action = dfplayer_ns.class_("PlayMp3Action", automation.Action) PlayFileAction = dfplayer_ns.class_("PlayFileAction", automation.Action) PlayFolderAction = dfplayer_ns.class_("PlayFolderAction", automation.Action) SetVolumeAction = dfplayer_ns.class_("SetVolumeAction", automation.Action) @@ -113,6 +114,25 @@ async def dfplayer_previous_to_code(config, action_id, template_arg, args): return var +@automation.register_action( + "dfplayer.play_mp3", + PlayMp3Action, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_FILE): cv.templatable(cv.int_), + }, + key=CONF_FILE, + ), +) +async def dfplayer_play_mp3_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_FILE], args, float) + cg.add(var.set_file(template_)) + return var + + @automation.register_action( "dfplayer.play", PlayFileAction, @@ -348,7 +368,7 @@ async def dfplayer_random_to_code(config, action_id, template_arg, args): } ), ) -async def dfplyaer_is_playing_to_code(config, condition_id, template_arg, args): +async def dfplayer_is_playing_to_code(config, condition_id, template_arg, args): var = cg.new_Pvariable(condition_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index 7df551f5d2..a6339dc988 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -7,10 +7,10 @@ namespace dfplayer { static const char *const TAG = "dfplayer"; void DFPlayer::play_folder(uint16_t folder, uint16_t file) { - if (folder < 100 && file < 256) { + if (folder <= 10 && file <= 1000) { this->ack_set_is_playing_ = true; this->send_cmd_(0x0F, (uint8_t) folder, (uint8_t) file); - } else if (folder <= 10 && file <= 1000) { + } else if (folder < 100 && file < 256) { this->ack_set_is_playing_ = true; this->send_cmd_(0x14, (((uint16_t) folder) << 12) | file); } else { @@ -19,7 +19,7 @@ void DFPlayer::play_folder(uint16_t folder, uint16_t file) { } void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) { - uint8_t buffer[10]{0x7e, 0xff, 0x06, cmd, 0x01, (uint8_t)(argument >> 8), (uint8_t) argument, 0x00, 0x00, 0xef}; + uint8_t buffer[10]{0x7e, 0xff, 0x06, cmd, 0x01, (uint8_t) (argument >> 8), (uint8_t) argument, 0x00, 0x00, 0xef}; uint16_t checksum = 0; for (uint8_t i = 1; i < 7; i++) checksum += buffer[i]; @@ -77,14 +77,16 @@ void DFPlayer::loop() { case 0x3A: if (argument == 1) { ESP_LOGI(TAG, "USB loaded"); - } else if (argument == 2) + } else if (argument == 2) { ESP_LOGI(TAG, "TF Card loaded"); + } break; case 0x3B: if (argument == 1) { ESP_LOGI(TAG, "USB unloaded"); - } else if (argument == 2) + } else if (argument == 2) { ESP_LOGI(TAG, "TF Card unloaded"); + } break; case 0x3F: if (argument == 1) { diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index ae47cb33f1..26e90fd410 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -35,6 +35,10 @@ class DFPlayer : public uart::UARTDevice, public Component { this->ack_set_is_playing_ = true; this->send_cmd_(0x02); } + void play_mp3(uint16_t file) { + this->ack_set_is_playing_ = true; + this->send_cmd_(0x12, file); + } void play_file(uint16_t file) { this->ack_set_is_playing_ = true; this->send_cmd_(0x03, file); @@ -113,6 +117,16 @@ class DFPlayer : public uart::UARTDevice, public Component { DFPLAYER_SIMPLE_ACTION(NextAction, next) DFPLAYER_SIMPLE_ACTION(PreviousAction, previous) +template class PlayMp3Action : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, file) + + void play(Ts... x) override { + auto file = this->file_.value(x...); + this->parent_->play_mp3(file); + } +}; + template class PlayFileAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint16_t, file) diff --git a/esphome/components/dht12/dht12.h b/esphome/components/dht12/dht12.h index ae4d4fd607..2a706039ba 100644 --- a/esphome/components/dht12/dht12.h +++ b/esphome/components/dht12/dht12.h @@ -20,8 +20,8 @@ class DHT12Component : public PollingComponent, public i2c::I2CDevice { protected: bool read_data_(uint8_t *data); - sensor::Sensor *temperature_sensor_; - sensor::Sensor *humidity_sensor_; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; }; } // namespace dht12 diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 4ad353a254..86e8624d33 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -3,9 +3,9 @@ #include #include "esphome/core/application.h" #include "esphome/core/color.h" -#include "esphome/core/log.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" namespace esphome { namespace display { @@ -24,6 +24,7 @@ void DisplayBuffer::init_internal_(uint32_t buffer_length) { } this->clear(); } + void DisplayBuffer::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } void DisplayBuffer::clear() { this->fill(COLOR_OFF); } int DisplayBuffer::get_width() { @@ -50,6 +51,9 @@ int DisplayBuffer::get_height() { } void DisplayBuffer::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; } void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) { + if (!this->get_clipping().inside(x, y)) + return; // NOLINT + switch (this->rotation_) { case DISPLAY_ROTATION_0_DEGREES: break; @@ -161,88 +165,53 @@ void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, Color } while (dx <= 0); } -void DisplayBuffer::print(int x, int y, Font *font, Color color, TextAlign align, const char *text) { +void DisplayBuffer::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text) { int x_start, y_start; int width, height; this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height); - - int i = 0; - int x_at = x_start; - while (text[i] != '\0') { - int match_length; - int glyph_n = font->match_next_glyph(text + i, &match_length); - if (glyph_n < 0) { - // Unknown char, skip - ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); - if (!font->get_glyphs().empty()) { - uint8_t glyph_width = font->get_glyphs()[0].glyph_data_->width; - for (int glyph_x = 0; glyph_x < glyph_width; glyph_x++) { - for (int glyph_y = 0; glyph_y < height; glyph_y++) - this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color); - } - x_at += glyph_width; - } - - i++; - continue; - } - - const Glyph &glyph = font->get_glyphs()[glyph_n]; - int scan_x1, scan_y1, scan_width, scan_height; - glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height); - - for (int glyph_x = scan_x1; glyph_x < scan_x1 + scan_width; glyph_x++) { - for (int glyph_y = scan_y1; glyph_y < scan_y1 + scan_height; glyph_y++) { - if (glyph.get_pixel(glyph_x, glyph_y)) { - this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color); - } - } - } - - x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; - - i += match_length; - } + font->print(x_start, y_start, this, color, text); } -void DisplayBuffer::vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg) { +void DisplayBuffer::vprintf_(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, + va_list arg) { char buffer[256]; int ret = vsnprintf(buffer, sizeof(buffer), format, arg); if (ret > 0) this->print(x, y, font, color, align, buffer); } -void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color color_off) { - switch (image->get_type()) { - case IMAGE_TYPE_BINARY: - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - this->draw_pixel_at(x + img_x, y + img_y, image->get_pixel(img_x, img_y) ? color_on : color_off); - } - } +void DisplayBuffer::image(int x, int y, BaseImage *image, Color color_on, Color color_off) { + this->image(x, y, image, ImageAlign::TOP_LEFT, color_on, color_off); +} + +void DisplayBuffer::image(int x, int y, BaseImage *image, ImageAlign align, Color color_on, Color color_off) { + auto x_align = ImageAlign(int(align) & (int(ImageAlign::HORIZONTAL_ALIGNMENT))); + auto y_align = ImageAlign(int(align) & (int(ImageAlign::VERTICAL_ALIGNMENT))); + + switch (x_align) { + case ImageAlign::RIGHT: + x -= image->get_width(); break; - case IMAGE_TYPE_GRAYSCALE: - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - this->draw_pixel_at(x + img_x, y + img_y, image->get_grayscale_pixel(img_x, img_y)); - } - } + case ImageAlign::CENTER_HORIZONTAL: + x -= image->get_width() / 2; break; - case IMAGE_TYPE_RGB24: - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - this->draw_pixel_at(x + img_x, y + img_y, image->get_color_pixel(img_x, img_y)); - } - } - break; - case IMAGE_TYPE_TRANSPARENT_BINARY: - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - if (image->get_pixel(img_x, img_y)) - this->draw_pixel_at(x + img_x, y + img_y, color_on); - } - } + case ImageAlign::LEFT: + default: break; } + + switch (y_align) { + case ImageAlign::BOTTOM: + y -= image->get_height(); + break; + case ImageAlign::CENTER_VERTICAL: + y -= image->get_height() / 2; + break; + case ImageAlign::TOP: + default: + break; + } + + image->draw(x, y, this, color_on, color_off); } #ifdef USE_GRAPH @@ -258,7 +227,7 @@ void DisplayBuffer::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_ } #endif // USE_QR_CODE -void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1, +void DisplayBuffer::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1, int *width, int *height) { int x_offset, baseline; font->measure(text, width, &x_offset, &baseline, height); @@ -296,34 +265,34 @@ void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font, break; } } -void DisplayBuffer::print(int x, int y, Font *font, Color color, const char *text) { +void DisplayBuffer::print(int x, int y, BaseFont *font, Color color, const char *text) { this->print(x, y, font, color, TextAlign::TOP_LEFT, text); } -void DisplayBuffer::print(int x, int y, Font *font, TextAlign align, const char *text) { +void DisplayBuffer::print(int x, int y, BaseFont *font, TextAlign align, const char *text) { this->print(x, y, font, COLOR_ON, align, text); } -void DisplayBuffer::print(int x, int y, Font *font, const char *text) { +void DisplayBuffer::print(int x, int y, BaseFont *font, const char *text) { this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text); } -void DisplayBuffer::printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) { +void DisplayBuffer::printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, color, align, format, arg); va_end(arg); } -void DisplayBuffer::printf(int x, int y, Font *font, Color color, const char *format, ...) { +void DisplayBuffer::printf(int x, int y, BaseFont *font, Color color, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, color, TextAlign::TOP_LEFT, format, arg); va_end(arg); } -void DisplayBuffer::printf(int x, int y, Font *font, TextAlign align, const char *format, ...) { +void DisplayBuffer::printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, COLOR_ON, align, format, arg); va_end(arg); } -void DisplayBuffer::printf(int x, int y, Font *font, const char *format, ...) { +void DisplayBuffer::printf(int x, int y, BaseFont *font, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, arg); @@ -361,195 +330,65 @@ void DisplayBuffer::do_update_() { } else if (this->writer_.has_value()) { (*this->writer_)(*this); } + // remove all not ended clipping regions + while (is_clipping()) { + end_clipping(); + } } void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) { if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to)) this->trigger(from, to); } -#ifdef USE_TIME -void DisplayBuffer::strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, - time::ESPTime time) { +void DisplayBuffer::strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, + ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) this->print(x, y, font, color, align, buffer); } -void DisplayBuffer::strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) { +void DisplayBuffer::strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) { this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time); } -void DisplayBuffer::strftime(int x, int y, Font *font, TextAlign align, const char *format, time::ESPTime time) { +void DisplayBuffer::strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time) { this->strftime(x, y, font, COLOR_ON, align, format, time); } -void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, time::ESPTime time) { +void DisplayBuffer::strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) { this->strftime(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, time); } -#endif -bool Glyph::get_pixel(int x, int y) const { - const int x_data = x - this->glyph_data_->offset_x; - const int y_data = y - this->glyph_data_->offset_y; - if (x_data < 0 || x_data >= this->glyph_data_->width || y_data < 0 || y_data >= this->glyph_data_->height) - return false; - const uint32_t width_8 = ((this->glyph_data_->width + 7u) / 8u) * 8u; - const uint32_t pos = x_data + y_data * width_8; - return progmem_read_byte(this->glyph_data_->data + (pos / 8u)) & (0x80 >> (pos % 8u)); -} -const char *Glyph::get_char() const { return this->glyph_data_->a_char; } -bool Glyph::compare_to(const char *str) const { - // 1 -> this->char_ - // 2 -> str - for (uint32_t i = 0;; i++) { - if (this->glyph_data_->a_char[i] == '\0') - return true; - if (str[i] == '\0') - return false; - if (this->glyph_data_->a_char[i] > str[i]) - return false; - if (this->glyph_data_->a_char[i] < str[i]) - return true; +void DisplayBuffer::start_clipping(Rect rect) { + if (!this->clipping_rectangle_.empty()) { + Rect r = this->clipping_rectangle_.back(); + rect.shrink(r); } - // this should not happen - return false; + this->clipping_rectangle_.push_back(rect); } -int Glyph::match_length(const char *str) const { - for (uint32_t i = 0;; i++) { - if (this->glyph_data_->a_char[i] == '\0') - return i; - if (str[i] != this->glyph_data_->a_char[i]) - return 0; +void DisplayBuffer::end_clipping() { + if (this->clipping_rectangle_.empty()) { + ESP_LOGE(TAG, "clear: Clipping is not set."); + } else { + this->clipping_rectangle_.pop_back(); } - // this should not happen - return 0; } -void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { - *x1 = this->glyph_data_->offset_x; - *y1 = this->glyph_data_->offset_y; - *width = this->glyph_data_->width; - *height = this->glyph_data_->height; -} -int Font::match_next_glyph(const char *str, int *match_length) { - int lo = 0; - int hi = this->glyphs_.size() - 1; - while (lo != hi) { - int mid = (lo + hi + 1) / 2; - if (this->glyphs_[mid].compare_to(str)) { - lo = mid; - } else { - hi = mid - 1; - } +void DisplayBuffer::extend_clipping(Rect add_rect) { + if (this->clipping_rectangle_.empty()) { + ESP_LOGE(TAG, "add: Clipping is not set."); + } else { + this->clipping_rectangle_.back().extend(add_rect); } - *match_length = this->glyphs_[lo].match_length(str); - if (*match_length <= 0) - return -1; - return lo; } -void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) { - *baseline = this->baseline_; - *height = this->bottom_; - int i = 0; - int min_x = 0; - bool has_char = false; - int x = 0; - while (str[i] != '\0') { - int match_length; - int glyph_n = this->match_next_glyph(str + i, &match_length); - if (glyph_n < 0) { - // Unknown char, skip - if (!this->get_glyphs().empty()) - x += this->get_glyphs()[0].glyph_data_->width; - i++; - continue; - } - - const Glyph &glyph = this->glyphs_[glyph_n]; - if (!has_char) { - min_x = glyph.glyph_data_->offset_x; - } else { - min_x = std::min(min_x, x + glyph.glyph_data_->offset_x); - } - x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; - - i += match_length; - has_char = true; +void DisplayBuffer::shrink_clipping(Rect add_rect) { + if (this->clipping_rectangle_.empty()) { + ESP_LOGE(TAG, "add: Clipping is not set."); + } else { + this->clipping_rectangle_.back().shrink(add_rect); } - *x_offset = min_x; - *width = x - min_x; } -const std::vector &Font::get_glyphs() const { return this->glyphs_; } -Font::Font(const GlyphData *data, int data_nr, int baseline, int bottom) : baseline_(baseline), bottom_(bottom) { - for (int i = 0; i < data_nr; ++i) - glyphs_.emplace_back(data + i); -} - -bool Image::get_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return false; - const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; - const uint32_t pos = x + y * width_8; - return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); -} -Color Image::get_color_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_) * 3; - const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) | - (progmem_read_byte(this->data_start_ + pos + 1) << 8) | - (progmem_read_byte(this->data_start_ + pos + 0) << 16); - return Color(color32); -} -Color Image::get_grayscale_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_); - const uint8_t gray = progmem_read_byte(this->data_start_ + pos); - return Color(gray | gray << 8 | gray << 16 | gray << 24); -} -int Image::get_width() const { return this->width_; } -int Image::get_height() const { return this->height_; } -ImageType Image::get_type() const { return this->type_; } -Image::Image(const uint8_t *data_start, int width, int height, ImageType type) - : width_(width), height_(height), type_(type), data_start_(data_start) {} - -bool Animation::get_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return false; - const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; - const uint32_t frame_index = this->height_ * width_8 * this->current_frame_; - if (frame_index >= (uint32_t)(this->width_ * this->height_ * this->animation_frame_count_)) - return false; - const uint32_t pos = x + y * width_8 + frame_index; - return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); -} -Color Animation::get_color_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; - if (frame_index >= (uint32_t)(this->width_ * this->height_ * this->animation_frame_count_)) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_ + frame_index) * 3; - const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) | - (progmem_read_byte(this->data_start_ + pos + 1) << 8) | - (progmem_read_byte(this->data_start_ + pos + 0) << 16); - return Color(color32); -} -Color Animation::get_grayscale_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; - if (frame_index >= (uint32_t)(this->width_ * this->height_ * this->animation_frame_count_)) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_ + frame_index); - const uint8_t gray = progmem_read_byte(this->data_start_ + pos); - return Color(gray | gray << 8 | gray << 16 | gray << 24); -} -Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type) - : Image(data_start, width, height, type), current_frame_(0), animation_frame_count_(animation_frame_count) {} -int Animation::get_animation_frame_count() const { return this->animation_frame_count_; } -int Animation::get_current_frame() const { return this->current_frame_; } -void Animation::next_frame() { - this->current_frame_++; - if (this->current_frame_ >= animation_frame_count_) { - this->current_frame_ = 0; +Rect DisplayBuffer::get_clipping() { + if (this->clipping_rectangle_.empty()) { + return Rect(); + } else { + return this->clipping_rectangle_.back(); } } diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 8ee1cd8779..1a62da2323 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -1,14 +1,13 @@ #pragma once +#include +#include +#include "rect.h" +#include "display_color_utils.h" +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" -#include "esphome/core/automation.h" -#include "display_color_utils.h" -#include - -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif +#include "esphome/core/time.h" #ifdef USE_GRAPH #include "esphome/components/graph/graph.h" @@ -72,16 +71,58 @@ enum class TextAlign { BOTTOM_RIGHT = BOTTOM | RIGHT, }; -/// Turn the pixel OFF. -extern const Color COLOR_OFF; -/// Turn the pixel ON. -extern const Color COLOR_ON; +/** ImageAlign is used to tell the display class how to position a image. By default + * the coordinates you enter for the image() functions take the upper left corner of the image + * as the "anchor" point. You can customize this behavior to, for example, make the coordinates + * refer to the *center* of the image. + * + * All image alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis + * these options are allowed: + * + * - LEFT (x-coordinate of anchor point is on left) + * - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the image) + * - RIGHT (x-coordinate of anchor point is on right) + * + * For the Y-Axis alignment these options are allowed: + * + * - TOP (y-coordinate of anchor is on the top of the image) + * - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the image) + * - BOTTOM (y-coordinate of anchor is on the bottom of the image) + * + * These options are then combined to create combined TextAlignment options like: + * - TOP_LEFT (default) + * - CENTER (anchor point is in the middle of the image bounds) + * - ... + */ +enum class ImageAlign { + TOP = 0x00, + CENTER_VERTICAL = 0x01, + BOTTOM = 0x02, -enum ImageType { - IMAGE_TYPE_BINARY = 0, - IMAGE_TYPE_GRAYSCALE = 1, - IMAGE_TYPE_RGB24 = 2, - IMAGE_TYPE_TRANSPARENT_BINARY = 3, + LEFT = 0x00, + CENTER_HORIZONTAL = 0x04, + RIGHT = 0x08, + + TOP_LEFT = TOP | LEFT, + TOP_CENTER = TOP | CENTER_HORIZONTAL, + TOP_RIGHT = TOP | RIGHT, + + CENTER_LEFT = CENTER_VERTICAL | LEFT, + CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL, + CENTER_RIGHT = CENTER_VERTICAL | RIGHT, + + BOTTOM_LEFT = BOTTOM | LEFT, + BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL, + BOTTOM_RIGHT = BOTTOM | RIGHT, + + HORIZONTAL_ALIGNMENT = LEFT | CENTER_HORIZONTAL | RIGHT, + VERTICAL_ALIGNMENT = TOP | CENTER_VERTICAL | BOTTOM +}; + +enum DisplayType { + DISPLAY_TYPE_BINARY = 1, + DISPLAY_TYPE_GRAYSCALE = 2, + DISPLAY_TYPE_COLOR = 3, }; enum DisplayRotation { @@ -91,8 +132,6 @@ enum DisplayRotation { DISPLAY_ROTATION_270_DEGREES = 270, }; -class Font; -class Image; class DisplayBuffer; class DisplayPage; class DisplayOnPageChangeTrigger; @@ -106,6 +145,24 @@ using display_writer_t = std::function; ESP_LOGCONFIG(TAG, "%s Dimensions: %dpx x %dpx", prefix, (obj)->get_width(), (obj)->get_height()); \ } +/// Turn the pixel OFF. +extern const Color COLOR_OFF; +/// Turn the pixel ON. +extern const Color COLOR_ON; + +class BaseImage { + public: + virtual void draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) = 0; + virtual int get_width() const = 0; + virtual int get_height() const = 0; +}; + +class BaseFont { + public: + virtual void print(int x, int y, DisplayBuffer *display, Color color, const char *text) = 0; + virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0; +}; + class DisplayBuffer { public: /// Fill the entire screen with the given color. @@ -117,6 +174,7 @@ class DisplayBuffer { int get_width(); /// Get the height of the image in pixels with rotation applied. int get_height(); + /// Set a single pixel at the specified coordinates to the given color. void draw_pixel_at(int x, int y, Color color = COLOR_ON); @@ -151,7 +209,7 @@ class DisplayBuffer { * @param align The alignment of the text. * @param text The text to draw. */ - void print(int x, int y, Font *font, Color color, TextAlign align, const char *text); + void print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text); /** Print `text` with the top left at [x,y] with `font`. * @@ -161,7 +219,7 @@ class DisplayBuffer { * @param color The color to draw the text with. * @param text The text to draw. */ - void print(int x, int y, Font *font, Color color, const char *text); + void print(int x, int y, BaseFont *font, Color color, const char *text); /** Print `text` with the anchor point at [x,y] with `font`. * @@ -171,7 +229,7 @@ class DisplayBuffer { * @param align The alignment of the text. * @param text The text to draw. */ - void print(int x, int y, Font *font, TextAlign align, const char *text); + void print(int x, int y, BaseFont *font, TextAlign align, const char *text); /** Print `text` with the top left at [x,y] with `font`. * @@ -180,7 +238,7 @@ class DisplayBuffer { * @param font The font to draw the text with. * @param text The text to draw. */ - void print(int x, int y, Font *font, const char *text); + void print(int x, int y, BaseFont *font, const char *text); /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. * @@ -192,7 +250,7 @@ class DisplayBuffer { * @param format The format to use. * @param ... The arguments to use for the text formatting. */ - void printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) + void printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...) __attribute__((format(printf, 7, 8))); /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`. @@ -204,7 +262,7 @@ class DisplayBuffer { * @param format The format to use. * @param ... The arguments to use for the text formatting. */ - void printf(int x, int y, Font *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7))); + void printf(int x, int y, BaseFont *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7))); /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. * @@ -215,7 +273,8 @@ class DisplayBuffer { * @param format The format to use. * @param ... The arguments to use for the text formatting. */ - void printf(int x, int y, Font *font, TextAlign align, const char *format, ...) __attribute__((format(printf, 6, 7))); + void printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) + __attribute__((format(printf, 6, 7))); /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`. * @@ -225,9 +284,8 @@ class DisplayBuffer { * @param format The format to use. * @param ... The arguments to use for the text formatting. */ - void printf(int x, int y, Font *font, const char *format, ...) __attribute__((format(printf, 5, 6))); + void printf(int x, int y, BaseFont *font, const char *format, ...) __attribute__((format(printf, 5, 6))); -#ifdef USE_TIME /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. * * @param x The x coordinate of the text alignment anchor point. @@ -238,7 +296,7 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, time::ESPTime time) + void strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time) __attribute__((format(strftime, 7, 0))); /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. @@ -250,7 +308,7 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) + void strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) __attribute__((format(strftime, 6, 0))); /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. @@ -262,7 +320,7 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, TextAlign align, const char *format, time::ESPTime time) + void strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time) __attribute__((format(strftime, 6, 0))); /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. @@ -273,19 +331,28 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, const char *format, time::ESPTime time) - __attribute__((format(strftime, 5, 0))); -#endif + void strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) __attribute__((format(strftime, 5, 0))); /** Draw the `image` with the top-left corner at [x,y] to the screen. * * @param x The x coordinate of the upper left corner. * @param y The y coordinate of the upper left corner. - * @param image The image to draw + * @param image The image to draw. * @param color_on The color to replace in binary images for the on bits. * @param color_off The color to replace in binary images for the off bits. */ - void image(int x, int y, Image *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); + void image(int x, int y, BaseImage *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); + + /** Draw the `image` at [x,y] to the screen. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param image The image to draw. + * @param align The alignment of the image. + * @param color_on The color to replace in binary images for the on bits. + * @param color_off The color to replace in binary images for the off bits. + */ + void image(int x, int y, BaseImage *image, ImageAlign align, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); #ifdef USE_GRAPH /** Draw the `graph` with the top-left corner at [x,y] to the screen. @@ -334,7 +401,7 @@ class DisplayBuffer { * @param width A pointer to store the returned text width in. * @param height A pointer to store the returned text height in. */ - void get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1, int *width, + void get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1, int *width, int *height); /// Internal method to set the display writer lambda. @@ -360,8 +427,56 @@ class DisplayBuffer { virtual int get_width_internal() = 0; DisplayRotation get_rotation() const { return this->rotation_; } + /** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays, + * returns the type the display is currently configured to. + */ + virtual DisplayType get_display_type() = 0; + + /** Set the clipping rectangle for further drawing + * + * @param[in] rect: Pointer to Rect for clipping (or NULL for entire screen) + * + * return true if success, false if error + */ + void start_clipping(Rect rect); + void start_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) { + start_clipping(Rect(left, top, right - left, bottom - top)); + }; + + /** Add a rectangular region to the invalidation region + * - This is usually called when an element has been modified + * + * @param[in] rect: Rectangle to add to the invalidation region + */ + void extend_clipping(Rect rect); + void extend_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) { + this->extend_clipping(Rect(left, top, right - left, bottom - top)); + }; + + /** substract a rectangular region to the invalidation region + * - This is usually called when an element has been modified + * + * @param[in] rect: Rectangle to add to the invalidation region + */ + void shrink_clipping(Rect rect); + void shrink_clipping(uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) { + this->shrink_clipping(Rect(left, top, right - left, bottom - top)); + }; + + /** Reset the invalidation region + */ + void end_clipping(); + + /** Get the current the clipping rectangle + * + * return rect for active clipping region + */ + Rect get_clipping(); + + bool is_clipping() const { return !this->clipping_rectangle_.empty(); } + protected: - void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg); + void vprintf_(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, va_list arg); virtual void draw_absolute_pixel_internal(int x, int y, Color color) = 0; @@ -376,6 +491,7 @@ class DisplayBuffer { DisplayPage *previous_page_{nullptr}; std::vector on_page_change_triggers_; bool auto_clear_enabled_{true}; + std::vector clipping_rectangle_; }; class DisplayPage { @@ -396,91 +512,6 @@ class DisplayPage { DisplayPage *next_{nullptr}; }; -struct GlyphData { - const char *a_char; - const uint8_t *data; - int offset_x; - int offset_y; - int width; - int height; -}; - -class Glyph { - public: - Glyph(const GlyphData *data) : glyph_data_(data) {} - - bool get_pixel(int x, int y) const; - - const char *get_char() const; - - bool compare_to(const char *str) const; - - int match_length(const char *str) const; - - void scan_area(int *x1, int *y1, int *width, int *height) const; - - protected: - friend Font; - friend DisplayBuffer; - - const GlyphData *glyph_data_; -}; - -class Font { - public: - /** Construct the font with the given glyphs. - * - * @param glyphs A vector of glyphs, must be sorted lexicographically. - * @param baseline The y-offset from the top of the text to the baseline. - * @param bottom The y-offset from the top of the text to the bottom (i.e. height). - */ - Font(const GlyphData *data, int data_nr, int baseline, int bottom); - - int match_next_glyph(const char *str, int *match_length); - - void measure(const char *str, int *width, int *x_offset, int *baseline, int *height); - - const std::vector &get_glyphs() const; - - protected: - std::vector glyphs_; - int baseline_; - int bottom_; -}; - -class Image { - public: - Image(const uint8_t *data_start, int width, int height, ImageType type); - virtual bool get_pixel(int x, int y) const; - virtual Color get_color_pixel(int x, int y) const; - virtual Color get_grayscale_pixel(int x, int y) const; - int get_width() const; - int get_height() const; - ImageType get_type() const; - - protected: - int width_; - int height_; - ImageType type_; - const uint8_t *data_start_; -}; - -class Animation : public Image { - public: - Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type); - bool get_pixel(int x, int y) const override; - Color get_color_pixel(int x, int y) const override; - Color get_grayscale_pixel(int x, int y) const override; - - int get_animation_frame_count() const; - int get_current_frame() const; - void next_frame(); - - protected: - int current_frame_; - int animation_frame_count_; -}; - template class DisplayPageShowAction : public Action { public: TEMPLATABLE_VALUE(DisplayPage *, page) diff --git a/esphome/components/display/display_color_utils.h b/esphome/components/display/display_color_utils.h index 202de912de..3114dee359 100644 --- a/esphome/components/display/display_color_utils.h +++ b/esphome/components/display/display_color_utils.h @@ -66,6 +66,9 @@ class ColorUtil { } return color_return; } + static inline Color rgb332_to_color(uint8_t rgb332_color) { + return to_color((uint32_t) rgb332_color, COLOR_ORDER_RGB, COLOR_BITNESS_332); + } static uint8_t color_to_332(Color color, ColorOrder color_order = ColorOrder::COLOR_ORDER_RGB) { uint16_t red_color, green_color, blue_color; @@ -100,11 +103,57 @@ class ColorUtil { } return 0; } - static uint32_t color_to_grayscale4(Color color) { uint32_t gs4 = esp_scale8(color.white, 15); return gs4; } + /*** + * Converts a Color value to an 8bit index using a 24bit 888 palette. + * Uses euclidiean distance to calculate the linear distance between + * two points in an RGB cube, then iterates through the full palette + * returning the closest match. + * @param[in] color The target color. + * @param[in] palette The 256*3 byte RGB palette. + * @return The 8 bit index of the closest color (e.g. for display buffer). + */ + // static uint8_t color_to_index8_palette888(Color color, uint8_t *palette) { + static uint8_t color_to_index8_palette888(Color color, const uint8_t *palette) { + uint8_t closest_index = 0; + uint32_t minimum_dist2 = UINT32_MAX; // Smallest distance^2 to the target + // so far + // int8_t(*plt)[][3] = palette; + int16_t tgt_r = color.r; + int16_t tgt_g = color.g; + int16_t tgt_b = color.b; + uint16_t x, y, z; + // Loop through each row of the palette + for (uint16_t i = 0; i < 256; i++) { + // Get the pallet rgb color + int16_t plt_r = (int16_t) palette[i * 3 + 0]; + int16_t plt_g = (int16_t) palette[i * 3 + 1]; + int16_t plt_b = (int16_t) palette[i * 3 + 2]; + // Calculate euclidean distance (linear distance in rgb cube). + x = (uint32_t) std::abs(tgt_r - plt_r); + y = (uint32_t) std::abs(tgt_g - plt_g); + z = (uint32_t) std::abs(tgt_b - plt_b); + uint32_t dist2 = x * x + y * y + z * z; + if (dist2 < minimum_dist2) { + minimum_dist2 = dist2; + closest_index = (uint8_t) i; + } + } + return closest_index; + } + /*** + * Converts an 8bit palette index (e.g. from a display buffer) to a color. + * @param[in] index The index to look up. + * @param[in] palette The 256*3 byte RGB palette. + * @return The RGBW Color object looked up by the palette. + */ + static Color index8_to_color_palette888(uint8_t index, const uint8_t *palette) { + Color color = Color(palette[index * 3 + 0], palette[index * 3 + 1], palette[index * 3 + 2], 0); + return color; + } }; } // namespace display } // namespace esphome diff --git a/esphome/components/display/rect.cpp b/esphome/components/display/rect.cpp new file mode 100644 index 0000000000..6e91c86c4f --- /dev/null +++ b/esphome/components/display/rect.cpp @@ -0,0 +1,98 @@ +#include "rect.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace display { + +static const char *const TAG = "display"; + +void Rect::expand(int16_t horizontal, int16_t vertical) { + if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) { + this->x = this->x - horizontal; + this->y = this->y - vertical; + this->w = this->w + (2 * horizontal); + this->h = this->h + (2 * vertical); + } +} + +void Rect::extend(Rect rect) { + if (!this->is_set()) { + this->x = rect.x; + this->y = rect.y; + this->w = rect.w; + this->h = rect.h; + } else { + if (this->x > rect.x) { + this->w = this->w + (this->x - rect.x); + this->x = rect.x; + } + if (this->y > rect.y) { + this->h = this->h + (this->y - rect.y); + this->y = rect.y; + } + if (this->x2() < rect.x2()) { + this->w = rect.x2() - this->x; + } + if (this->y2() < rect.y2()) { + this->h = rect.y2() - this->y; + } + } +} +void Rect::shrink(Rect rect) { + if (!this->inside(rect)) { + (*this) = Rect(); + } else { + if (this->x2() > rect.x2()) { + this->w = rect.x2() - this->x; + } + if (this->x < rect.x) { + this->w = this->w + (this->x - rect.x); + this->x = rect.x; + } + if (this->y2() > rect.y2()) { + this->h = rect.y2() - this->y; + } + if (this->y < rect.y) { + this->h = this->h + (this->y - rect.y); + this->y = rect.y; + } + } +} + +bool Rect::equal(Rect rect) { + return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h); +} + +bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) { // NOLINT + if (!this->is_set()) { + return true; + } + if (absolute) { + return ((test_x >= this->x) && (test_x <= this->x2()) && (test_y >= this->y) && (test_y <= this->y2())); + } else { + return ((test_x >= 0) && (test_x <= this->w) && (test_y >= 0) && (test_y <= this->h)); + } +} + +bool Rect::inside(Rect rect, bool absolute) { + if (!this->is_set() || !rect.is_set()) { + return true; + } + if (absolute) { + return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y)); + } else { + return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0)); + } +} + +void Rect::info(const std::string &prefix) { + if (this->is_set()) { + ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(), + this->y2()); + } else + ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str()); +} + +} // namespace display +} // namespace esphome diff --git a/esphome/components/display/rect.h b/esphome/components/display/rect.h new file mode 100644 index 0000000000..867a9c67c7 --- /dev/null +++ b/esphome/components/display/rect.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/helpers.h" + +namespace esphome { +namespace display { + +static const int16_t VALUE_NO_SET = 32766; + +class Rect { + public: + int16_t x; ///< X coordinate of corner + int16_t y; ///< Y coordinate of corner + int16_t w; ///< Width of region + int16_t h; ///< Height of region + + Rect() : x(VALUE_NO_SET), y(VALUE_NO_SET), w(VALUE_NO_SET), h(VALUE_NO_SET) {} // NOLINT + inline Rect(int16_t x, int16_t y, int16_t w, int16_t h) ALWAYS_INLINE : x(x), y(y), w(w), h(h) {} + inline int16_t x2() { return this->x + this->w; }; ///< X coordinate of corner + inline int16_t y2() { return this->y + this->h; }; ///< Y coordinate of corner + + inline bool is_set() ALWAYS_INLINE { return (this->h != VALUE_NO_SET) && (this->w != VALUE_NO_SET); } + + void expand(int16_t horizontal, int16_t vertical); + + void extend(Rect rect); + void shrink(Rect rect); + + bool inside(Rect rect, bool absolute = true); + bool inside(int16_t test_x, int16_t test_y, bool absolute = true); + bool equal(Rect rect); + void info(const std::string &prefix = "rect info:"); +}; + +} // namespace display +} // namespace esphome diff --git a/esphome/components/display_menu_base/__init__.py b/esphome/components/display_menu_base/__init__.py new file mode 100644 index 0000000000..d7326cdc65 --- /dev/null +++ b/esphome/components/display_menu_base/__init__.py @@ -0,0 +1,430 @@ +import re +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation, core +from esphome.const import ( + CONF_ID, + CONF_TYPE, + CONF_TRIGGER_ID, + CONF_ON_VALUE, + CONF_COMMAND, + CONF_CUSTOM, + CONF_NUMBER, + CONF_FORMAT, + CONF_MODE, + CONF_ACTIVE, +) +from esphome.automation import maybe_simple_id +from esphome.components.select import Select +from esphome.components.number import Number +from esphome.components.switch import Switch + +CODEOWNERS = ["@numo68"] + +display_menu_base_ns = cg.esphome_ns.namespace("display_menu_base") + +CONF_DISPLAY_ID = "display_id" + +CONF_ROTARY = "rotary" +CONF_JOYSTICK = "joystick" +CONF_LABEL = "label" +CONF_MENU = "menu" +CONF_BACK = "back" +CONF_TEXT = "text" +CONF_SELECT = "select" +CONF_SWITCH = "switch" +CONF_ITEMS = "items" +CONF_ON_TEXT = "on_text" +CONF_OFF_TEXT = "off_text" +CONF_VALUE_LAMBDA = "value_lambda" +CONF_IMMEDIATE_EDIT = "immediate_edit" +CONF_ROOT_ITEM_ID = "root_item_id" +CONF_ON_ENTER = "on_enter" +CONF_ON_LEAVE = "on_leave" +CONF_ON_NEXT = "on_next" +CONF_ON_PREV = "on_prev" + +DisplayMenuComponent = display_menu_base_ns.class_("DisplayMenuComponent", cg.Component) + +MenuItem = display_menu_base_ns.class_("MenuItem") +MenuItemConstPtr = MenuItem.operator("ptr").operator("const") +MenuItemMenu = display_menu_base_ns.class_("MenuItemMenu") +MenuItemSelect = display_menu_base_ns.class_("MenuItemSelect") +MenuItemNumber = display_menu_base_ns.class_("MenuItemNumber") +MenuItemSwitch = display_menu_base_ns.class_("MenuItemSwitch") +MenuItemCommand = display_menu_base_ns.class_("MenuItemCommand") +MenuItemCustom = display_menu_base_ns.class_("MenuItemCustom") + +UpAction = display_menu_base_ns.class_("UpAction", automation.Action) +DownAction = display_menu_base_ns.class_("DownAction", automation.Action) +LeftAction = display_menu_base_ns.class_("LeftAction", automation.Action) +RightAction = display_menu_base_ns.class_("RightAction", automation.Action) +EnterAction = display_menu_base_ns.class_("EnterAction", automation.Action) +ShowAction = display_menu_base_ns.class_("ShowAction", automation.Action) +HideAction = display_menu_base_ns.class_("HideAction", automation.Action) +ShowMainAction = display_menu_base_ns.class_("ShowMainAction", automation.Action) + +IsActiveCondition = display_menu_base_ns.class_( + "IsActiveCondition", automation.Condition +) + +MULTI_CONF = True + +MenuItemType = display_menu_base_ns.enum("MenuItemType") + +MENU_ITEM_TYPES = { + CONF_LABEL: MenuItemType.MENU_ITEM_LABEL, + CONF_MENU: MenuItemType.MENU_ITEM_MENU, + CONF_BACK: MenuItemType.MENU_ITEM_BACK, + CONF_SELECT: MenuItemType.MENU_ITEM_SELECT, + CONF_NUMBER: MenuItemType.MENU_ITEM_NUMBER, + CONF_SWITCH: MenuItemType.MENU_ITEM_SWITCH, + CONF_COMMAND: MenuItemType.MENU_ITEM_COMMAND, + CONF_CUSTOM: MenuItemType.MENU_ITEM_CUSTOM, +} + +MENU_ITEMS_WITH_SPECIALIZED_CLASSES = ( + CONF_MENU, + CONF_SELECT, + CONF_NUMBER, + CONF_SWITCH, + CONF_COMMAND, + CONF_CUSTOM, +) + +MenuMode = display_menu_base_ns.enum("MenuMode") + +MENU_MODES = { + CONF_ROTARY: MenuMode.MENU_MODE_ROTARY, + CONF_JOYSTICK: MenuMode.MENU_MODE_JOYSTICK, +} + +DisplayMenuOnEnterTrigger = display_menu_base_ns.class_( + "DisplayMenuOnEnterTrigger", automation.Trigger +) + +DisplayMenuOnLeaveTrigger = display_menu_base_ns.class_( + "DisplayMenuOnLeaveTrigger", automation.Trigger +) + +DisplayMenuOnValueTrigger = display_menu_base_ns.class_( + "DisplayMenuOnValueTrigger", automation.Trigger +) + +DisplayMenuOnNextTrigger = display_menu_base_ns.class_( + "DisplayMenuOnNextTrigger", automation.Trigger +) + +DisplayMenuOnPrevTrigger = display_menu_base_ns.class_( + "DisplayMenuOnPrevTrigger", automation.Trigger +) + + +def validate_format(format): + if re.search(r"^%([+-])*(\d+)*(\.\d+)*[fg]$", format) is None: + raise cv.Invalid( + f"{CONF_FORMAT}: has to specify a printf-like format string specifying exactly one f or g type conversion, '{format}' provided" + ) + + return format + + +# Use a simple indirection to circumvent the recursion limitation +def menu_item_schema(value): + return MENU_ITEM_SCHEMA(value) + + +MENU_ITEM_COMMON_SCHEMA = cv.Schema( + { + cv.Optional(CONF_TEXT): cv.templatable(cv.string), + } +) + +MENU_ITEM_ENTER_LEAVE_SCHEMA = MENU_ITEM_COMMON_SCHEMA.extend( + { + cv.Optional(CONF_ON_ENTER): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + DisplayMenuOnEnterTrigger + ), + } + ), + cv.Optional(CONF_ON_LEAVE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + DisplayMenuOnLeaveTrigger + ), + } + ), + } +) + +MENU_ITEM_VALUE_SCHEMA = MENU_ITEM_COMMON_SCHEMA.extend( + { + cv.Optional(CONF_ON_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + DisplayMenuOnValueTrigger + ), + } + ), + } +) + +MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA = MENU_ITEM_ENTER_LEAVE_SCHEMA.extend( + { + cv.Optional(CONF_ON_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + DisplayMenuOnValueTrigger + ), + } + ), + } +) + +MENU_ITEM_SCHEMA = cv.typed_schema( + { + CONF_LABEL: MENU_ITEM_COMMON_SCHEMA.extend( + { + cv.GenerateID(CONF_ID): cv.declare_id(MenuItem), + } + ), + CONF_BACK: MENU_ITEM_COMMON_SCHEMA.extend( + { + cv.GenerateID(CONF_ID): cv.declare_id(MenuItem), + } + ), + CONF_MENU: MENU_ITEM_ENTER_LEAVE_SCHEMA.extend( + { + cv.GenerateID(CONF_ID): cv.declare_id(MenuItemMenu), + cv.Required(CONF_ITEMS): cv.All( + cv.ensure_list(menu_item_schema), cv.Length(min=1) + ), + } + ), + CONF_SELECT: MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA.extend( + { + cv.GenerateID(CONF_ID): cv.declare_id(MenuItemSelect), + cv.Required(CONF_SELECT): cv.use_id(Select), + cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean, + cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda, + } + ), + CONF_NUMBER: MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA.extend( + { + cv.GenerateID(CONF_ID): cv.declare_id(MenuItemNumber), + cv.Required(CONF_NUMBER): cv.use_id(Number), + cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean, + cv.Optional(CONF_FORMAT, default="%.1f"): cv.All( + cv.string_strict, + validate_format, + ), + cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda, + } + ), + CONF_SWITCH: MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA.extend( + { + cv.GenerateID(CONF_ID): cv.declare_id(MenuItemSwitch), + cv.Required(CONF_SWITCH): cv.use_id(Switch), + cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean, + cv.Optional(CONF_ON_TEXT, default="On"): cv.string_strict, + cv.Optional(CONF_OFF_TEXT, default="Off"): cv.string_strict, + cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda, + } + ), + CONF_COMMAND: MENU_ITEM_VALUE_SCHEMA.extend( + { + cv.GenerateID(CONF_ID): cv.declare_id(MenuItemCommand), + } + ), + CONF_CUSTOM: MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA.extend( + { + cv.GenerateID(CONF_ID): cv.declare_id(MenuItemCustom), + cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean, + cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_ON_NEXT): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + DisplayMenuOnNextTrigger + ), + } + ), + cv.Optional(CONF_ON_PREV): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + DisplayMenuOnPrevTrigger + ), + } + ), + } + ), + }, + default_type="label", + lower=True, +) + +DISPLAY_MENU_BASE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_ACTIVE, default=True): cv.boolean, + cv.GenerateID(CONF_ROOT_ITEM_ID): cv.declare_id(MenuItemMenu), + cv.Optional(CONF_MODE, default=CONF_ROTARY): cv.enum(MENU_MODES), + cv.Optional(CONF_ON_ENTER): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + DisplayMenuOnEnterTrigger + ), + } + ), + cv.Optional(CONF_ON_LEAVE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + DisplayMenuOnLeaveTrigger + ), + } + ), + cv.Required(CONF_ITEMS): cv.All( + cv.ensure_list(MENU_ITEM_SCHEMA), cv.Length(min=1) + ), + } +).extend(cv.COMPONENT_SCHEMA) + +MENU_ACTION_SCHEMA = maybe_simple_id( + { + cv.GenerateID(CONF_ID): cv.use_id(DisplayMenuComponent), + } +) + + +@automation.register_action("display_menu.up", UpAction, MENU_ACTION_SCHEMA) +async def menu_up_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action("display_menu.down", DownAction, MENU_ACTION_SCHEMA) +async def menu_down_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action("display_menu.left", LeftAction, MENU_ACTION_SCHEMA) +async def menu_left_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action("display_menu.right", RightAction, MENU_ACTION_SCHEMA) +async def menu_right_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action("display_menu.enter", EnterAction, MENU_ACTION_SCHEMA) +async def menu_enter_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action("display_menu.show", ShowAction, MENU_ACTION_SCHEMA) +async def menu_show_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action("display_menu.hide", HideAction, MENU_ACTION_SCHEMA) +async def menu_hide_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action( + "display_menu.show_main", ShowMainAction, MENU_ACTION_SCHEMA +) +async def menu_show_main_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_condition( + "display_menu.is_active", + IsActiveCondition, + automation.maybe_simple_id( + { + cv.GenerateID(CONF_ID): cv.use_id(DisplayMenuComponent), + } + ), +) +async def display_menu_is_active_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren) + + +async def menu_item_to_code(menu, config, parent): + if config[CONF_TYPE] in MENU_ITEMS_WITH_SPECIALIZED_CLASSES: + item = cg.new_Pvariable(config[CONF_ID]) + else: + item = cg.new_Pvariable(config[CONF_ID], MENU_ITEM_TYPES[config[CONF_TYPE]]) + cg.add(parent.add_item(item)) + if CONF_TEXT in config: + if isinstance(config[CONF_TEXT], core.Lambda): + template_ = await cg.templatable( + config[CONF_TEXT], [(MenuItemConstPtr, "it")], cg.std_string + ) + cg.add(item.set_text(template_)) + else: + cg.add(item.set_text(config[CONF_TEXT])) + if CONF_VALUE_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_VALUE_LAMBDA], + [(MenuItemConstPtr, "it")], + return_type=cg.std_string, + ) + cg.add(item.set_value_lambda(template_)) + if CONF_ITEMS in config: + for c in config[CONF_ITEMS]: + await menu_item_to_code(menu, c, item) + if CONF_IMMEDIATE_EDIT in config: + cg.add(item.set_immediate_edit(config[CONF_IMMEDIATE_EDIT])) + if config[CONF_TYPE] == CONF_SELECT: + var = await cg.get_variable(config[CONF_SELECT]) + cg.add(item.set_select_variable(var)) + if config[CONF_TYPE] == CONF_NUMBER: + var = await cg.get_variable(config[CONF_NUMBER]) + cg.add(item.set_number_variable(var)) + cg.add(item.set_format(config[CONF_FORMAT])) + if config[CONF_TYPE] == CONF_SWITCH: + var = await cg.get_variable(config[CONF_SWITCH]) + cg.add(item.set_switch_variable(var)) + cg.add(item.set_on_text(config[CONF_ON_TEXT])) + cg.add(item.set_off_text(config[CONF_OFF_TEXT])) + for conf in config.get(CONF_ON_ENTER, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item) + await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf) + for conf in config.get(CONF_ON_LEAVE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item) + await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf) + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item) + await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf) + for conf in config.get(CONF_ON_NEXT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item) + await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf) + for conf in config.get(CONF_ON_PREV, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item) + await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf) + + +async def display_menu_to_code(menu, config): + cg.add(menu.set_active(config[CONF_ACTIVE])) + root_item = cg.new_Pvariable(config[CONF_ROOT_ITEM_ID]) + cg.add(menu.set_root_item(root_item)) + cg.add(menu.set_mode(config[CONF_MODE])) + for c in config[CONF_ITEMS]: + await menu_item_to_code(menu, c, root_item) + for conf in config.get(CONF_ON_ENTER, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], root_item) + await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf) + for conf in config.get(CONF_ON_LEAVE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], root_item) + await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf) diff --git a/esphome/components/display_menu_base/automation.h b/esphome/components/display_menu_base/automation.h new file mode 100644 index 0000000000..d5394a1e0c --- /dev/null +++ b/esphome/components/display_menu_base/automation.h @@ -0,0 +1,133 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "display_menu_base.h" + +namespace esphome { +namespace display_menu_base { + +template class UpAction : public Action { + public: + explicit UpAction(DisplayMenuComponent *menu) : menu_(menu) {} + + void play(Ts... x) override { this->menu_->up(); } + + protected: + DisplayMenuComponent *menu_; +}; + +template class DownAction : public Action { + public: + explicit DownAction(DisplayMenuComponent *menu) : menu_(menu) {} + + void play(Ts... x) override { this->menu_->down(); } + + protected: + DisplayMenuComponent *menu_; +}; + +template class LeftAction : public Action { + public: + explicit LeftAction(DisplayMenuComponent *menu) : menu_(menu) {} + + void play(Ts... x) override { this->menu_->left(); } + + protected: + DisplayMenuComponent *menu_; +}; + +template class RightAction : public Action { + public: + explicit RightAction(DisplayMenuComponent *menu) : menu_(menu) {} + + void play(Ts... x) override { this->menu_->right(); } + + protected: + DisplayMenuComponent *menu_; +}; + +template class EnterAction : public Action { + public: + explicit EnterAction(DisplayMenuComponent *menu) : menu_(menu) {} + + void play(Ts... x) override { this->menu_->enter(); } + + protected: + DisplayMenuComponent *menu_; +}; + +template class ShowAction : public Action { + public: + explicit ShowAction(DisplayMenuComponent *menu) : menu_(menu) {} + + void play(Ts... x) override { this->menu_->show(); } + + protected: + DisplayMenuComponent *menu_; +}; + +template class HideAction : public Action { + public: + explicit HideAction(DisplayMenuComponent *menu) : menu_(menu) {} + + void play(Ts... x) override { this->menu_->hide(); } + + protected: + DisplayMenuComponent *menu_; +}; + +template class ShowMainAction : public Action { + public: + explicit ShowMainAction(DisplayMenuComponent *menu) : menu_(menu) {} + + void play(Ts... x) override { this->menu_->show_main(); } + + protected: + DisplayMenuComponent *menu_; +}; +template class IsActiveCondition : public Condition { + public: + explicit IsActiveCondition(DisplayMenuComponent *menu) : menu_(menu) {} + bool check(Ts... x) override { return this->menu_->is_active(); } + + protected: + DisplayMenuComponent *menu_; +}; + +class DisplayMenuOnEnterTrigger : public Trigger { + public: + explicit DisplayMenuOnEnterTrigger(MenuItem *parent) { + parent->add_on_enter_callback([this, parent]() { this->trigger(parent); }); + } +}; + +class DisplayMenuOnLeaveTrigger : public Trigger { + public: + explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) { + parent->add_on_leave_callback([this, parent]() { this->trigger(parent); }); + } +}; + +class DisplayMenuOnValueTrigger : public Trigger { + public: + explicit DisplayMenuOnValueTrigger(MenuItem *parent) { + parent->add_on_value_callback([this, parent]() { this->trigger(parent); }); + } +}; + +class DisplayMenuOnNextTrigger : public Trigger { + public: + explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) { + parent->add_on_next_callback([this, parent]() { this->trigger(parent); }); + } +}; + +class DisplayMenuOnPrevTrigger : public Trigger { + public: + explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) { + parent->add_on_prev_callback([this, parent]() { this->trigger(parent); }); + } +}; + +} // namespace display_menu_base +} // namespace esphome diff --git a/esphome/components/display_menu_base/display_menu_base.cpp b/esphome/components/display_menu_base/display_menu_base.cpp new file mode 100644 index 0000000000..57da3cec35 --- /dev/null +++ b/esphome/components/display_menu_base/display_menu_base.cpp @@ -0,0 +1,315 @@ +#include "display_menu_base.h" +#include + +namespace esphome { +namespace display_menu_base { + +void DisplayMenuComponent::up() { + if (this->check_healthy_and_active_()) { + bool changed = false; + + if (this->editing_) { + switch (this->mode_) { + case MENU_MODE_ROTARY: + changed = this->get_selected_item_()->select_prev(); + break; + default: + break; + } + } else { + changed = this->cursor_up_(); + } + + if (changed) + this->draw_and_update(); + } +} + +void DisplayMenuComponent::down() { + if (this->check_healthy_and_active_()) { + bool changed = false; + + if (this->editing_) { + switch (this->mode_) { + case MENU_MODE_ROTARY: + changed = this->get_selected_item_()->select_next(); + break; + default: + break; + } + } else { + changed = this->cursor_down_(); + } + + if (changed) + this->draw_and_update(); + } +} + +void DisplayMenuComponent::left() { + if (this->check_healthy_and_active_()) { + bool changed = false; + + switch (this->get_selected_item_()->get_type()) { + case MENU_ITEM_SELECT: + case MENU_ITEM_SWITCH: + case MENU_ITEM_NUMBER: + case MENU_ITEM_CUSTOM: + switch (this->mode_) { + case MENU_MODE_ROTARY: + if (this->editing_) { + this->finish_editing_(); + changed = true; + } + break; + case MENU_MODE_JOYSTICK: + if (this->editing_ || this->get_selected_item_()->get_immediate_edit()) + changed = this->get_selected_item_()->select_prev(); + break; + default: + break; + } + break; + case MENU_ITEM_BACK: + changed = this->leave_menu_(); + break; + default: + break; + } + + if (changed) + this->draw_and_update(); + } +} + +void DisplayMenuComponent::right() { + if (this->check_healthy_and_active_()) { + bool changed = false; + + switch (this->get_selected_item_()->get_type()) { + case MENU_ITEM_SELECT: + case MENU_ITEM_SWITCH: + case MENU_ITEM_NUMBER: + case MENU_ITEM_CUSTOM: + switch (this->mode_) { + case MENU_MODE_JOYSTICK: + if (this->editing_ || this->get_selected_item_()->get_immediate_edit()) + changed = this->get_selected_item_()->select_next(); + default: + break; + } + break; + case MENU_ITEM_MENU: + changed = this->enter_menu_(); + break; + default: + break; + } + + if (changed) + this->draw_and_update(); + } +} + +void DisplayMenuComponent::enter() { + if (this->check_healthy_and_active_()) { + bool changed = false; + MenuItem *item = this->get_selected_item_(); + + if (this->editing_) { + this->finish_editing_(); + changed = true; + } else { + switch (item->get_type()) { + case MENU_ITEM_MENU: + changed = this->enter_menu_(); + break; + case MENU_ITEM_BACK: + changed = this->leave_menu_(); + break; + case MENU_ITEM_SELECT: + case MENU_ITEM_SWITCH: + case MENU_ITEM_CUSTOM: + if (item->get_immediate_edit()) { + changed = item->select_next(); + } else { + this->editing_ = true; + item->on_enter(); + changed = true; + } + break; + case MENU_ITEM_NUMBER: + // A number cannot be immediate in the rotary mode + if (!item->get_immediate_edit() || this->mode_ == MENU_MODE_ROTARY) { + this->editing_ = true; + item->on_enter(); + changed = true; + } + break; + case MENU_ITEM_COMMAND: + changed = item->select_next(); + break; + default: + break; + } + } + + if (changed) + this->draw_and_update(); + } +} + +void DisplayMenuComponent::draw() { + if (this->check_healthy_and_active_()) + this->draw_menu(); +} + +void DisplayMenuComponent::show_main() { + bool disp_changed = false; + + if (this->is_failed()) + return; + + this->process_initial_(); + + if (this->active_ && this->editing_) + this->finish_editing_(); + + if (this->displayed_item_ != this->root_item_) { + this->displayed_item_->on_leave(); + disp_changed = true; + } + + this->reset_(); + this->active_ = true; + + if (disp_changed) { + this->displayed_item_->on_enter(); + } + + this->draw_and_update(); +} + +void DisplayMenuComponent::show() { + if (this->is_failed()) + return; + + this->process_initial_(); + + if (!this->active_) { + this->active_ = true; + this->draw_and_update(); + } +} + +void DisplayMenuComponent::hide() { + if (this->check_healthy_and_active_()) { + if (this->editing_) + this->finish_editing_(); + this->active_ = false; + this->update(); + } +} + +void DisplayMenuComponent::reset_() { + this->displayed_item_ = this->root_item_; + this->cursor_index_ = this->top_index_ = 0; + this->selection_stack_.clear(); +} + +void DisplayMenuComponent::process_initial_() { + if (!this->root_on_enter_called_) { + this->root_item_->on_enter(); + this->root_on_enter_called_ = true; + } +} + +bool DisplayMenuComponent::check_healthy_and_active_() { + if (this->is_failed()) + return false; + + this->process_initial_(); + + return this->active_; +} + +bool DisplayMenuComponent::cursor_up_() { + bool changed = false; + + if (this->cursor_index_ > 0) { + changed = true; + + --this->cursor_index_; + + if (this->cursor_index_ < this->top_index_) + this->top_index_ = this->cursor_index_; + } + + return changed; +} + +bool DisplayMenuComponent::cursor_down_() { + bool changed = false; + + if (this->cursor_index_ + 1 < this->displayed_item_->items_size()) { + changed = true; + + ++this->cursor_index_; + + if (this->cursor_index_ >= this->top_index_ + this->rows_) + this->top_index_ = this->cursor_index_ - this->rows_ + 1; + } + + return changed; +} + +bool DisplayMenuComponent::enter_menu_() { + this->displayed_item_->on_leave(); + this->displayed_item_ = static_cast(this->get_selected_item_()); + this->selection_stack_.push_front({this->top_index_, this->cursor_index_}); + this->cursor_index_ = this->top_index_ = 0; + this->displayed_item_->on_enter(); + + return true; +} + +bool DisplayMenuComponent::leave_menu_() { + bool changed = false; + + if (this->displayed_item_->get_parent() != nullptr) { + this->displayed_item_->on_leave(); + this->displayed_item_ = this->displayed_item_->get_parent(); + this->top_index_ = this->selection_stack_.front().first; + this->cursor_index_ = this->selection_stack_.front().second; + this->selection_stack_.pop_front(); + this->displayed_item_->on_enter(); + changed = true; + } + + return changed; +} + +void DisplayMenuComponent::finish_editing_() { + switch (this->get_selected_item_()->get_type()) { + case MENU_ITEM_SELECT: + case MENU_ITEM_NUMBER: + case MENU_ITEM_SWITCH: + case MENU_ITEM_CUSTOM: + this->get_selected_item_()->on_leave(); + break; + default: + break; + } + + this->editing_ = false; +} + +void DisplayMenuComponent::draw_menu() { + for (size_t i = 0; i < this->rows_ && this->top_index_ + i < this->displayed_item_->items_size(); ++i) { + this->draw_item(this->displayed_item_->get_item(this->top_index_ + i), i, + this->top_index_ + i == this->cursor_index_); + } +} + +} // namespace display_menu_base +} // namespace esphome diff --git a/esphome/components/display_menu_base/display_menu_base.h b/esphome/components/display_menu_base/display_menu_base.h new file mode 100644 index 0000000000..46bb0a8192 --- /dev/null +++ b/esphome/components/display_menu_base/display_menu_base.h @@ -0,0 +1,77 @@ +#pragma once + +#include "esphome/core/component.h" + +#include "menu_item.h" + +#include + +namespace esphome { +namespace display_menu_base { + +enum MenuMode { + MENU_MODE_ROTARY, + MENU_MODE_JOYSTICK, +}; + +class MenuItem; + +/** Class to display a hierarchical menu. + * + */ +class DisplayMenuComponent : public Component { + public: + void set_root_item(MenuItemMenu *item) { this->displayed_item_ = this->root_item_ = item; } + void set_active(bool active) { this->active_ = active; } + void set_mode(MenuMode mode) { this->mode_ = mode; } + void set_rows(uint8_t rows) { this->rows_ = rows; } + + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + + void up(); + void down(); + void left(); + void right(); + void enter(); + + void show_main(); + void show(); + void hide(); + + void draw(); + + bool is_active() const { return this->active_; } + + protected: + void reset_(); + void process_initial_(); + bool check_healthy_and_active_(); + MenuItem *get_selected_item_() { return this->displayed_item_->get_item(this->cursor_index_); } + bool cursor_up_(); + bool cursor_down_(); + bool enter_menu_(); + bool leave_menu_(); + void finish_editing_(); + virtual void draw_menu(); + virtual void draw_item(const MenuItem *item, uint8_t row, bool selected) = 0; + virtual void update() {} + virtual void draw_and_update() { + draw_menu(); + update(); + } + + uint8_t rows_; + bool active_; + MenuMode mode_; + MenuItemMenu *root_item_{nullptr}; + + MenuItemMenu *displayed_item_{nullptr}; + uint8_t top_index_{0}; + uint8_t cursor_index_{0}; + std::forward_list> selection_stack_{}; + bool editing_{false}; + bool root_on_enter_called_{false}; +}; + +} // namespace display_menu_base +} // namespace esphome diff --git a/esphome/components/display_menu_base/menu_item.cpp b/esphome/components/display_menu_base/menu_item.cpp new file mode 100644 index 0000000000..bbe6ec0e89 --- /dev/null +++ b/esphome/components/display_menu_base/menu_item.cpp @@ -0,0 +1,179 @@ +#include "menu_item.h" + +#include + +namespace esphome { +namespace display_menu_base { + +void MenuItem::on_enter() { this->on_enter_callbacks_.call(); } + +void MenuItem::on_leave() { this->on_leave_callbacks_.call(); } + +void MenuItem::on_value_() { this->on_value_callbacks_.call(); } + +#ifdef USE_SELECT +std::string MenuItemSelect::get_value_text() const { + std::string result; + + if (this->value_getter_.has_value()) { + result = this->value_getter_.value()(this); + } else { + if (this->select_var_ != nullptr) { + result = this->select_var_->state; + } + } + + return result; +} + +bool MenuItemSelect::select_next() { + bool changed = false; + + if (this->select_var_ != nullptr) { + this->select_var_->make_call().select_next(true).perform(); + changed = true; + } + + return changed; +} + +bool MenuItemSelect::select_prev() { + bool changed = false; + + if (this->select_var_ != nullptr) { + this->select_var_->make_call().select_previous(true).perform(); + changed = true; + } + + return changed; +} +#endif // USE_SELECT + +#ifdef USE_NUMBER +std::string MenuItemNumber::get_value_text() const { + std::string result; + + if (this->value_getter_.has_value()) { + result = this->value_getter_.value()(this); + } else { + char data[32]; + snprintf(data, sizeof(data), this->format_.c_str(), get_number_value_()); + result = data; + } + + return result; +} + +bool MenuItemNumber::select_next() { + bool changed = false; + + if (this->number_var_ != nullptr) { + float last = this->number_var_->state; + this->number_var_->make_call().number_increment(false).perform(); + + if (this->number_var_->state != last) { + this->on_value_(); + changed = true; + } + } + + return changed; +} + +bool MenuItemNumber::select_prev() { + bool changed = false; + + if (this->number_var_ != nullptr) { + float last = this->number_var_->state; + this->number_var_->make_call().number_decrement(false).perform(); + + if (this->number_var_->state != last) { + this->on_value_(); + changed = true; + } + } + + return changed; +} + +float MenuItemNumber::get_number_value_() const { + float result = 0.0; + + if (this->number_var_ != nullptr) { + if (!this->number_var_->has_state() || this->number_var_->state < this->number_var_->traits.get_min_value()) { + result = this->number_var_->traits.get_min_value(); + } else if (this->number_var_->state > this->number_var_->traits.get_max_value()) { + result = this->number_var_->traits.get_max_value(); + } else { + result = this->number_var_->state; + } + } + + return result; +} +#endif // USE_NUMBER + +#ifdef USE_SWITCH +std::string MenuItemSwitch::get_value_text() const { + std::string result; + + if (this->value_getter_.has_value()) { + result = this->value_getter_.value()(this); + } else { + result = this->get_switch_state_() ? this->switch_on_text_ : this->switch_off_text_; + } + + return result; +} + +bool MenuItemSwitch::select_next() { return this->toggle_switch_(); } + +bool MenuItemSwitch::select_prev() { return this->toggle_switch_(); } + +bool MenuItemSwitch::get_switch_state_() const { return (this->switch_var_ != nullptr && this->switch_var_->state); } + +bool MenuItemSwitch::toggle_switch_() { + bool changed = false; + + if (this->switch_var_ != nullptr) { + this->switch_var_->toggle(); + this->on_value_(); + changed = true; + } + + return changed; +} +#endif // USE_SWITCH + +std::string MenuItemCustom::get_value_text() const { + return (this->value_getter_.has_value()) ? this->value_getter_.value()(this) : ""; +} + +bool MenuItemCommand::select_next() { + this->on_value_(); + return true; +} + +bool MenuItemCommand::select_prev() { + this->on_value_(); + return true; +} + +bool MenuItemCustom::select_next() { + this->on_next_(); + this->on_value_(); + return true; +} + +bool MenuItemCustom::select_prev() { + this->on_prev_(); + this->on_value_(); + return true; +} + +void MenuItemCustom::on_next_() { this->on_next_callbacks_.call(); } + +void MenuItemCustom::on_prev_() { this->on_prev_callbacks_.call(); } + +} // namespace display_menu_base +} // namespace esphome diff --git a/esphome/components/display_menu_base/menu_item.h b/esphome/components/display_menu_base/menu_item.h new file mode 100644 index 0000000000..a30f31e88f --- /dev/null +++ b/esphome/components/display_menu_base/menu_item.h @@ -0,0 +1,187 @@ +#pragma once + +#include "esphome/core/defines.h" +#include "esphome/core/automation.h" + +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif + +#include + +namespace esphome { +namespace display_menu_base { + +enum MenuItemType { + MENU_ITEM_LABEL, + MENU_ITEM_MENU, + MENU_ITEM_BACK, + MENU_ITEM_SELECT, + MENU_ITEM_NUMBER, + MENU_ITEM_SWITCH, + MENU_ITEM_COMMAND, + MENU_ITEM_CUSTOM, +}; + +class MenuItem; +class MenuItemMenu; +using value_getter_t = std::function; + +class MenuItem { + public: + explicit MenuItem(MenuItemType t) : item_type_(t) {} + void set_parent(MenuItemMenu *parent) { this->parent_ = parent; } + MenuItemMenu *get_parent() { return this->parent_; } + MenuItemType get_type() const { return this->item_type_; } + template void set_text(V val) { this->text_ = val; } + void add_on_enter_callback(std::function &&cb) { this->on_enter_callbacks_.add(std::move(cb)); } + void add_on_leave_callback(std::function &&cb) { this->on_leave_callbacks_.add(std::move(cb)); } + void add_on_value_callback(std::function &&cb) { this->on_value_callbacks_.add(std::move(cb)); } + + std::string get_text() const { return const_cast(this)->text_.value(this); } + virtual bool get_immediate_edit() const { return false; } + virtual bool has_value() const { return false; } + virtual std::string get_value_text() const { return ""; } + + virtual bool select_next() { return false; } + virtual bool select_prev() { return false; } + + void on_enter(); + void on_leave(); + + protected: + void on_value_(); + + MenuItemType item_type_; + MenuItemMenu *parent_{nullptr}; + TemplatableValue text_; + + CallbackManager on_enter_callbacks_{}; + CallbackManager on_leave_callbacks_{}; + CallbackManager on_value_callbacks_{}; +}; + +class MenuItemMenu : public MenuItem { + public: + explicit MenuItemMenu() : MenuItem(MENU_ITEM_MENU) {} + void add_item(MenuItem *item) { + item->set_parent(this); + this->items_.push_back(item); + } + size_t items_size() const { return this->items_.size(); } + MenuItem *get_item(size_t i) { return this->items_[i]; } + + protected: + std::vector items_; +}; + +class MenuItemEditable : public MenuItem { + public: + explicit MenuItemEditable(MenuItemType t) : MenuItem(t) {} + void set_immediate_edit(bool val) { this->immediate_edit_ = val; } + bool get_immediate_edit() const override { return this->immediate_edit_; } + void set_value_lambda(value_getter_t &&getter) { this->value_getter_ = getter; } + + protected: + bool immediate_edit_{false}; + optional value_getter_{}; +}; + +#ifdef USE_SELECT +class MenuItemSelect : public MenuItemEditable { + public: + explicit MenuItemSelect() : MenuItemEditable(MENU_ITEM_SELECT) {} + void set_select_variable(select::Select *var) { this->select_var_ = var; } + + bool has_value() const override { return true; } + std::string get_value_text() const override; + + bool select_next() override; + bool select_prev() override; + + protected: + select::Select *select_var_{nullptr}; +}; +#endif + +#ifdef USE_NUMBER +class MenuItemNumber : public MenuItemEditable { + public: + explicit MenuItemNumber() : MenuItemEditable(MENU_ITEM_NUMBER) {} + void set_number_variable(number::Number *var) { this->number_var_ = var; } + void set_format(const std::string &fmt) { this->format_ = fmt; } + + bool has_value() const override { return true; } + std::string get_value_text() const override; + + bool select_next() override; + bool select_prev() override; + + protected: + float get_number_value_() const; + + number::Number *number_var_{nullptr}; + std::string format_; +}; +#endif + +#ifdef USE_SWITCH +class MenuItemSwitch : public MenuItemEditable { + public: + explicit MenuItemSwitch() : MenuItemEditable(MENU_ITEM_SWITCH) {} + void set_switch_variable(switch_::Switch *var) { this->switch_var_ = var; } + void set_on_text(const std::string &t) { this->switch_on_text_ = t; } + void set_off_text(const std::string &t) { this->switch_off_text_ = t; } + + bool has_value() const override { return true; } + std::string get_value_text() const override; + + bool select_next() override; + bool select_prev() override; + + protected: + bool get_switch_state_() const; + bool toggle_switch_(); + + switch_::Switch *switch_var_{nullptr}; + std::string switch_on_text_; + std::string switch_off_text_; +}; +#endif + +class MenuItemCommand : public MenuItem { + public: + explicit MenuItemCommand() : MenuItem(MENU_ITEM_COMMAND) {} + + bool select_next() override; + bool select_prev() override; +}; + +class MenuItemCustom : public MenuItemEditable { + public: + explicit MenuItemCustom() : MenuItemEditable(MENU_ITEM_CUSTOM) {} + void add_on_next_callback(std::function &&cb) { this->on_next_callbacks_.add(std::move(cb)); } + void add_on_prev_callback(std::function &&cb) { this->on_prev_callbacks_.add(std::move(cb)); } + + bool has_value() const override { return this->value_getter_.has_value(); } + std::string get_value_text() const override; + + bool select_next() override; + bool select_prev() override; + + protected: + void on_next_(); + void on_prev_(); + + CallbackManager on_next_callbacks_{}; + CallbackManager on_prev_callbacks_{}; +}; + +} // namespace display_menu_base +} // namespace esphome diff --git a/esphome/components/dps310/__init__.py b/esphome/components/dps310/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/dps310/dps310.cpp b/esphome/components/dps310/dps310.cpp new file mode 100644 index 0000000000..22fb52967f --- /dev/null +++ b/esphome/components/dps310/dps310.cpp @@ -0,0 +1,189 @@ +#include "dps310.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace dps310 { + +static const char *const TAG = "dps310"; + +void DPS310Component::setup() { + uint8_t coef_data_raw[DPS310_NUM_COEF_REGS]; + auto timer = DPS310_INIT_TIMEOUT; + uint8_t reg = 0; + + ESP_LOGCONFIG(TAG, "Setting up DPS310..."); + // first, reset the sensor + if (!this->write_byte(DPS310_REG_RESET, DPS310_CMD_RESET)) { + this->mark_failed(); + return; + } + delay(10); + // wait for the sensor and its coefficients to be ready + while (timer-- && (!(reg & DPS310_BIT_SENSOR_RDY) || !(reg & DPS310_BIT_COEF_RDY))) { + reg = this->read_byte(DPS310_REG_MEAS_CFG).value_or(0); + delay(5); + } + + if (!(reg & DPS310_BIT_SENSOR_RDY) || !(reg & DPS310_BIT_COEF_RDY)) { // the flags were not set in time + this->mark_failed(); + return; + } + // read device ID + if (!this->read_byte(DPS310_REG_PROD_REV_ID, &this->prod_rev_id_)) { + this->mark_failed(); + return; + } + // read in coefficients used to calculate the compensated pressure and temperature values + if (!this->read_bytes(DPS310_REG_COEF, coef_data_raw, DPS310_NUM_COEF_REGS)) { + this->mark_failed(); + return; + } + // read in coefficients source register, too -- we need this a few lines down + if (!this->read_byte(DPS310_REG_TMP_COEF_SRC, ®)) { + this->mark_failed(); + return; + } + // set up operational stuff + if (!this->write_byte(DPS310_REG_PRS_CFG, DPS310_VAL_PRS_CFG)) { + this->mark_failed(); + return; + } + if (!this->write_byte(DPS310_REG_TMP_CFG, DPS310_VAL_TMP_CFG | (reg & DPS310_BIT_TMP_COEF_SRC))) { + this->mark_failed(); + return; + } + if (!this->write_byte(DPS310_REG_CFG, DPS310_VAL_REG_CFG)) { + this->mark_failed(); + return; + } + if (!this->write_byte(DPS310_REG_MEAS_CFG, 0x07)) { // enable background mode + this->mark_failed(); + return; + } + + this->c0_ = // we only ever use c0/2, so just divide by 2 here to save time later + DPS310Component::twos_complement( + int16_t(((uint16_t) coef_data_raw[0] << 4) | (((uint16_t) coef_data_raw[1] >> 4) & 0x0F)), 12) / + 2; + + this->c1_ = + DPS310Component::twos_complement(int16_t((((uint16_t) coef_data_raw[1] & 0x0F) << 8) | coef_data_raw[2]), 12); + + this->c00_ = ((uint32_t) coef_data_raw[3] << 12) | ((uint32_t) coef_data_raw[4] << 4) | + (((uint32_t) coef_data_raw[5] >> 4) & 0x0F); + this->c00_ = DPS310Component::twos_complement(c00_, 20); + + this->c10_ = + (((uint32_t) coef_data_raw[5] & 0x0F) << 16) | ((uint32_t) coef_data_raw[6] << 8) | (uint32_t) coef_data_raw[7]; + this->c10_ = DPS310Component::twos_complement(c10_, 20); + + this->c01_ = int16_t(((uint16_t) coef_data_raw[8] << 8) | (uint16_t) coef_data_raw[9]); + this->c11_ = int16_t(((uint16_t) coef_data_raw[10] << 8) | (uint16_t) coef_data_raw[11]); + this->c20_ = int16_t(((uint16_t) coef_data_raw[12] << 8) | (uint16_t) coef_data_raw[13]); + this->c21_ = int16_t(((uint16_t) coef_data_raw[14] << 8) | (uint16_t) coef_data_raw[15]); + this->c30_ = int16_t(((uint16_t) coef_data_raw[16] << 8) | (uint16_t) coef_data_raw[17]); +} + +void DPS310Component::dump_config() { + ESP_LOGCONFIG(TAG, "DPS310:"); + ESP_LOGCONFIG(TAG, " Product ID: %u", this->prod_rev_id_ & 0x0F); + ESP_LOGCONFIG(TAG, " Revision ID: %u", (this->prod_rev_id_ >> 4) & 0x0F); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with DPS310 failed!"); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); +} + +float DPS310Component::get_setup_priority() const { return setup_priority::DATA; } + +void DPS310Component::update() { + if (!this->update_in_progress_) { + this->update_in_progress_ = true; + this->read_(); + } +} + +void DPS310Component::read_() { + uint8_t reg = 0; + if (!this->read_byte(DPS310_REG_MEAS_CFG, ®)) { + this->status_set_warning(); + return; + } + + if ((!this->got_pres_) && (reg & DPS310_BIT_PRS_RDY)) { + this->read_pressure_(); + } + + if ((!this->got_temp_) && (reg & DPS310_BIT_TMP_RDY)) { + this->read_temperature_(); + } + + if (this->got_pres_ && this->got_temp_) { + this->calculate_values_(this->raw_temperature_, this->raw_pressure_); + this->got_pres_ = false; + this->got_temp_ = false; + this->update_in_progress_ = false; + this->status_clear_warning(); + } else { + auto f = std::bind(&DPS310Component::read_, this); + this->set_timeout("dps310", 10, f); + } +} + +void DPS310Component::read_pressure_() { + uint8_t bytes[3]; + if (!this->read_bytes(DPS310_REG_PRS_B2, bytes, 3)) { + this->status_set_warning(); + return; + } + this->got_pres_ = true; + this->raw_pressure_ = DPS310Component::twos_complement( + int32_t((uint32_t(bytes[0]) << 16) | (uint32_t(bytes[1]) << 8) | (uint32_t(bytes[2]))), 24); +} + +void DPS310Component::read_temperature_() { + uint8_t bytes[3]; + if (!this->read_bytes(DPS310_REG_TMP_B2, bytes, 3)) { + this->status_set_warning(); + return; + } + this->got_temp_ = true; + this->raw_temperature_ = DPS310Component::twos_complement( + int32_t((uint32_t(bytes[0]) << 16) | (uint32_t(bytes[1]) << 8) | (uint32_t(bytes[2]))), 24); +} + +// Calculations are taken from the datasheet which can be found here: +// https://www.infineon.com/dgdl/Infineon-DPS310-DataSheet-v01_02-EN.pdf?fileId=5546d462576f34750157750826c42242 +// Sections "How to Calculate Compensated Pressure Values" and "How to Calculate Compensated Temperature Values" +// Variable names below match variable names from the datasheet but lowercased +void DPS310Component::calculate_values_(int32_t raw_temperature, int32_t raw_pressure) { + const float t_raw_sc = (float) raw_temperature / DPS310_SCALE_FACTOR; + const float p_raw_sc = (float) raw_pressure / DPS310_SCALE_FACTOR; + + const float temperature = t_raw_sc * this->c1_ + this->c0_; // c0/2 done earlier! + + const float pressure = (this->c00_ + p_raw_sc * (this->c10_ + p_raw_sc * (this->c20_ + p_raw_sc * this->c30_)) + + t_raw_sc * this->c01_ + t_raw_sc * p_raw_sc * (this->c11_ + p_raw_sc * this->c21_)) / + 100; // divide by 100 for hPa + + if (this->temperature_sensor_ != nullptr) { + this->temperature_sensor_->publish_state(temperature); + } + if (this->pressure_sensor_ != nullptr) { + this->pressure_sensor_->publish_state(pressure); + } +} + +int32_t DPS310Component::twos_complement(int32_t val, uint8_t bits) { + if (val & ((uint32_t) 1 << (bits - 1))) { + val -= (uint32_t) 1 << bits; + } + return val; +} + +} // namespace dps310 +} // namespace esphome diff --git a/esphome/components/dps310/dps310.h b/esphome/components/dps310/dps310.h new file mode 100644 index 0000000000..50e7d93c8a --- /dev/null +++ b/esphome/components/dps310/dps310.h @@ -0,0 +1,65 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace dps310 { + +static const uint8_t DPS310_REG_PRS_B2 = 0x00; // Highest byte of pressure data +static const uint8_t DPS310_REG_TMP_B2 = 0x03; // Highest byte of temperature data +static const uint8_t DPS310_REG_PRS_CFG = 0x06; // Pressure configuration +static const uint8_t DPS310_REG_TMP_CFG = 0x07; // Temperature configuration +static const uint8_t DPS310_REG_MEAS_CFG = 0x08; // Sensor configuration +static const uint8_t DPS310_REG_CFG = 0x09; // Interrupt/FIFO configuration +static const uint8_t DPS310_REG_RESET = 0x0C; // Soft reset +static const uint8_t DPS310_REG_PROD_REV_ID = 0x0D; // Register that contains the part ID +static const uint8_t DPS310_REG_COEF = 0x10; // Top of calibration coefficient data space +static const uint8_t DPS310_REG_TMP_COEF_SRC = 0x28; // Temperature calibration src + +static const uint8_t DPS310_BIT_PRS_RDY = 0x10; // Pressure measurement is ready +static const uint8_t DPS310_BIT_TMP_RDY = 0x20; // Temperature measurement is ready +static const uint8_t DPS310_BIT_SENSOR_RDY = 0x40; // Sensor initialization complete when bit is set +static const uint8_t DPS310_BIT_COEF_RDY = 0x80; // Coefficients are available when bit is set +static const uint8_t DPS310_BIT_TMP_COEF_SRC = 0x80; // Temperature measurement source (0 = ASIC, 1 = MEMS element) +static const uint8_t DPS310_BIT_REQ_PRES = 0x01; // Set this bit to request pressure reading +static const uint8_t DPS310_BIT_REQ_TEMP = 0x02; // Set this bit to request temperature reading + +static const uint8_t DPS310_CMD_RESET = 0x89; // What to write to reset the device + +static const uint8_t DPS310_VAL_PRS_CFG = 0x01; // Value written to DPS310_REG_PRS_CFG at startup +static const uint8_t DPS310_VAL_TMP_CFG = 0x01; // Value written to DPS310_REG_TMP_CFG at startup +static const uint8_t DPS310_VAL_REG_CFG = 0x00; // Value written to DPS310_REG_CFG at startup + +static const uint8_t DPS310_INIT_TIMEOUT = 20; // How long to wait for DPS310 start-up to complete +static const uint8_t DPS310_NUM_COEF_REGS = 18; // Number of coefficients we need to read from the device +static const int32_t DPS310_SCALE_FACTOR = 1572864; // Measurement compensation scale factor + +class DPS310Component : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } + + protected: + void read_(); + void read_pressure_(); + void read_temperature_(); + void calculate_values_(int32_t raw_temperature, int32_t raw_pressure); + static int32_t twos_complement(int32_t val, uint8_t bits); + + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + int32_t raw_pressure_, raw_temperature_, c00_, c10_; + int16_t c0_, c1_, c01_, c11_, c20_, c21_, c30_; + uint8_t prod_rev_id_; + bool got_pres_, got_temp_, update_in_progress_; +}; + +} // namespace dps310 +} // namespace esphome diff --git a/esphome/components/dps310/sensor.py b/esphome/components/dps310/sensor.py new file mode 100644 index 0000000000..742c873d9e --- /dev/null +++ b/esphome/components/dps310/sensor.py @@ -0,0 +1,62 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + ICON_GAUGE, + ICON_THERMOMETER, + UNIT_HECTOPASCAL, +) + +CODEOWNERS = ["@kbx81"] + +DEPENDENCIES = ["i2c"] + +dps310_ns = cg.esphome_ns.namespace("dps310") +DPS310Component = dps310_ns.class_( + "DPS310Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(DPS310Component), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + icon=ICON_GAUGE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x77)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_PRESSURE in config: + sens = await sensor.new_sensor(config[CONF_PRESSURE]) + cg.add(var.set_pressure_sensor(sens)) diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp index d249e9743a..472ccc7a9a 100644 --- a/esphome/components/ds1307/ds1307.cpp +++ b/esphome/components/ds1307/ds1307.cpp @@ -37,14 +37,14 @@ void DS1307Component::read_time() { ESP_LOGW(TAG, "RTC halted, not syncing to system clock."); return; } - time::ESPTime rtc_time{.second = uint8_t(ds1307_.reg.second + 10 * ds1307_.reg.second_10), - .minute = uint8_t(ds1307_.reg.minute + 10u * ds1307_.reg.minute_10), - .hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10), - .day_of_week = uint8_t(ds1307_.reg.weekday), - .day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10), - .day_of_year = 1, // ignored by recalc_timestamp_utc(false) - .month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10), - .year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000)}; + ESPTime rtc_time{.second = uint8_t(ds1307_.reg.second + 10 * ds1307_.reg.second_10), + .minute = uint8_t(ds1307_.reg.minute + 10u * ds1307_.reg.minute_10), + .hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10), + .day_of_week = uint8_t(ds1307_.reg.weekday), + .day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10), + .day_of_year = 1, // ignored by recalc_timestamp_utc(false) + .month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10), + .year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000)}; rtc_time.recalc_timestamp_utc(false); if (!rtc_time.is_valid()) { ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index 7a7681082e..d3d20ca2a7 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -10,6 +10,8 @@ from esphome.const import ( CODEOWNERS = ["@glmnet", "@zuidwijk"] +MULTI_CONF = True + DEPENDENCIES = ["uart"] AUTO_LOAD = ["sensor", "text_sensor"] @@ -17,6 +19,7 @@ CONF_CRC_CHECK = "crc_check" CONF_DECRYPTION_KEY = "decryption_key" CONF_DSMR_ID = "dsmr_id" CONF_GAS_MBUS_ID = "gas_mbus_id" +CONF_WATER_MBUS_ID = "water_mbus_id" CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length" CONF_REQUEST_INTERVAL = "request_interval" CONF_REQUEST_PIN = "request_pin" @@ -51,6 +54,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DECRYPTION_KEY): _validate_key, cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean, cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_, + cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_, cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_, cv.Optional(CONF_REQUEST_PIN): pins.gpio_output_pin_schema, cv.Optional( @@ -79,10 +83,11 @@ async def to_code(config): cg.add(var.set_request_interval(config[CONF_REQUEST_INTERVAL].total_milliseconds)) cg.add(var.set_receive_timeout(config[CONF_RECEIVE_TIMEOUT].total_milliseconds)) - cg.add_define("DSMR_GAS_MBUS_ID", config[CONF_GAS_MBUS_ID]) + cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID])) + cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID])) # DSMR Parser - cg.add_library("glmnet/Dsmr", "0.5") + cg.add_library("glmnet/Dsmr", "0.7") # Crypto - cg.add_library("rweather/Crypto", "0.2.0") + cg.add_library("rweather/Crypto", "0.4.0") diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp index 7b339e5fe0..f382730912 100644 --- a/esphome/components/dsmr/dsmr.cpp +++ b/esphome/components/dsmr/dsmr.cpp @@ -171,7 +171,7 @@ void Dsmr::receive_telegram_() { this->telegram_[this->bytes_read_] = c; this->bytes_read_++; - // Check for a footer, i.e. exlamation mark, followed by a hex checksum. + // Check for a footer, i.e. exclamation mark, followed by a hex checksum. if (c == '!') { ESP_LOGV(TAG, "Footer of telegram found"); this->footer_found_ = true; diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h index 76f79ee55c..6621d02cae 100644 --- a/esphome/components/dsmr/dsmr.h +++ b/esphome/components/dsmr/dsmr.h @@ -13,6 +13,8 @@ #include #include +#include + namespace esphome { namespace dsmr { diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index d809d0d105..2e2050ecab 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -2,19 +2,17 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( + CONF_ID, DEVICE_CLASS_CURRENT, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_GAS, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, + DEVICE_CLASS_WATER, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, UNIT_CUBIC_METER, - UNIT_EMPTY, UNIT_KILOWATT, UNIT_KILOWATT_HOURS, UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, @@ -30,202 +28,220 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr), cv.Optional("energy_delivered_lux"): sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("energy_delivered_tariff1"): sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("energy_delivered_tariff2"): sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("energy_returned_lux"): sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("energy_returned_tariff1"): sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("energy_returned_tariff2"): sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("total_imported_energy"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, - ICON_EMPTY, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=3, ), cv.Optional("total_exported_energy"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, - ICON_EMPTY, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=3, ), cv.Optional("power_delivered"): sensor.sensor_schema( - UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_returned"): sensor.sensor_schema( - UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_delivered"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE, - ICON_EMPTY, - 3, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE, - ICON_EMPTY, - 3, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("electricity_threshold"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=3, ), cv.Optional("electricity_switch_position"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=3, ), cv.Optional("electricity_failures"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, ), cv.Optional("electricity_long_failures"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, ), cv.Optional("electricity_sags_l1"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, ), cv.Optional("electricity_sags_l2"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, ), cv.Optional("electricity_sags_l3"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, ), cv.Optional("electricity_swells_l1"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, ), cv.Optional("electricity_swells_l2"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, ), cv.Optional("electricity_swells_l3"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, ), cv.Optional("current_l1"): sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("current_l2"): sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("current_l3"): sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_delivered_l1"): sensor.sensor_schema( - UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_delivered_l2"): sensor.sensor_schema( - UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_delivered_l3"): sensor.sensor_schema( - UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_returned_l1"): sensor.sensor_schema( - UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_returned_l2"): sensor.sensor_schema( - UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_returned_l3"): sensor.sensor_schema( - UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE, - ICON_EMPTY, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_delivered_l2"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE, - ICON_EMPTY, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_delivered_l3"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE, - ICON_EMPTY, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned_l1"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE, - ICON_EMPTY, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned_l2"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE, - ICON_EMPTY, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned_l3"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE, - ICON_EMPTY, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("voltage_l1"): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("voltage_l2"): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("voltage_l3"): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("gas_delivered"): sensor.sensor_schema( - UNIT_CUBIC_METER, - ICON_EMPTY, - 3, - DEVICE_CLASS_GAS, - STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_CUBIC_METER, + accuracy_decimals=3, + device_class=DEVICE_CLASS_GAS, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("gas_delivered_be"): sensor.sensor_schema( - UNIT_CUBIC_METER, - ICON_EMPTY, - 3, - DEVICE_CLASS_GAS, - STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_CUBIC_METER, + accuracy_decimals=3, + device_class=DEVICE_CLASS_GAS, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("water_delivered"): sensor.sensor_schema( + unit_of_measurement=UNIT_CUBIC_METER, + accuracy_decimals=3, + device_class=DEVICE_CLASS_WATER, + state_class=STATE_CLASS_TOTAL_INCREASING, ), } ).extend(cv.COMPONENT_SCHEMA) @@ -238,10 +254,10 @@ async def to_code(config): for key, conf in config.items(): if not isinstance(conf, dict): continue - id = conf.get("id") + id = conf[CONF_ID] if id and id.type == sensor.Sensor: - s = await sensor.new_sensor(conf) - cg.add(getattr(hub, f"set_{key}")(s)) + sens = await sensor.new_sensor(conf) + cg.add(getattr(hub, f"set_{key}")(sens)) sensors.append(f"F({key})") if sensors: diff --git a/esphome/components/dsmr/text_sensor.py b/esphome/components/dsmr/text_sensor.py index 339eea711f..202cc07020 100644 --- a/esphome/components/dsmr/text_sensor.py +++ b/esphome/components/dsmr/text_sensor.py @@ -1,9 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor -from esphome.const import ( - CONF_ID, -) + from . import Dsmr, CONF_DSMR_ID AUTO_LOAD = ["dsmr"] @@ -11,71 +9,19 @@ AUTO_LOAD = ["dsmr"] CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr), - cv.Optional("identification"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("p1_version"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("p1_version_be"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("timestamp"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("electricity_tariff"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("electricity_failure_log"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("message_short"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("message_long"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("gas_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("thermal_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("water_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("sub_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("gas_delivered_text"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), + cv.Optional("identification"): text_sensor.text_sensor_schema(), + cv.Optional("p1_version"): text_sensor.text_sensor_schema(), + cv.Optional("p1_version_be"): text_sensor.text_sensor_schema(), + cv.Optional("timestamp"): text_sensor.text_sensor_schema(), + cv.Optional("electricity_tariff"): text_sensor.text_sensor_schema(), + cv.Optional("electricity_failure_log"): text_sensor.text_sensor_schema(), + cv.Optional("message_short"): text_sensor.text_sensor_schema(), + cv.Optional("message_long"): text_sensor.text_sensor_schema(), + cv.Optional("gas_equipment_id"): text_sensor.text_sensor_schema(), + cv.Optional("thermal_equipment_id"): text_sensor.text_sensor_schema(), + cv.Optional("water_equipment_id"): text_sensor.text_sensor_schema(), + cv.Optional("sub_equipment_id"): text_sensor.text_sensor_schema(), + cv.Optional("gas_delivered_text"): text_sensor.text_sensor_schema(), } ).extend(cv.COMPONENT_SCHEMA) @@ -89,8 +35,7 @@ async def to_code(config): continue id = conf.get("id") if id and id.type == text_sensor.TextSensor: - var = cg.new_Pvariable(conf[CONF_ID]) - await text_sensor.register_text_sensor(var, conf) + var = await text_sensor.new_text_sensor(conf) cg.add(getattr(hub, f"set_{key}")(var)) text_sensors.append(f"F({key})") diff --git a/esphome/components/duty_cycle/sensor.py b/esphome/components/duty_cycle/sensor.py index 6a367328e6..3dcdf7a818 100644 --- a/esphome/components/duty_cycle/sensor.py +++ b/esphome/components/duty_cycle/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import sensor from esphome.const import ( - CONF_ID, CONF_PIN, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, @@ -17,25 +16,20 @@ DutyCycleSensor = duty_cycle_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( + DutyCycleSensor, unit_of_measurement=UNIT_PERCENT, icon=ICON_PERCENT, accuracy_decimals=1, state_class=STATE_CLASS_MEASUREMENT, ) - .extend( - { - cv.GenerateID(): cv.declare_id(DutyCycleSensor), - cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema), - } - ) + .extend({cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema)}) .extend(cv.polling_component_schema("60s")) ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) diff --git a/esphome/components/e131/__init__.py b/esphome/components/e131/__init__.py index bb662e0989..cec0bdf4fa 100644 --- a/esphome/components/e131/__init__.py +++ b/esphome/components/e131/__init__.py @@ -4,6 +4,7 @@ from esphome.components.light.types import AddressableLightEffect from esphome.components.light.effects import register_addressable_effect from esphome.const import CONF_ID, CONF_NAME, CONF_METHOD, CONF_CHANNELS +AUTO_LOAD = ["socket"] DEPENDENCIES = ["network"] e131_ns = cg.esphome_ns.namespace("e131") @@ -23,16 +24,11 @@ CHANNELS = { CONF_UNIVERSE = "universe" CONF_E131_ID = "e131_id" -CONFIG_SCHEMA = cv.All( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(E131Component), - cv.Optional(CONF_METHOD, default="MULTICAST"): cv.one_of( - *METHODS, upper=True - ), - } - ), - cv.only_with_arduino, +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(E131Component), + cv.Optional(CONF_METHOD, default="MULTICAST"): cv.one_of(*METHODS, upper=True), + } ) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index 6d584687ce..818006ced7 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -1,18 +1,7 @@ -#ifdef USE_ARDUINO - #include "e131.h" #include "e131_addressable_light_effect.h" #include "esphome/core/log.h" -#ifdef USE_ESP32 -#include -#endif - -#ifdef USE_ESP8266 -#include -#include -#endif - namespace esphome { namespace e131 { @@ -22,17 +11,41 @@ static const int PORT = 5568; E131Component::E131Component() {} E131Component::~E131Component() { - if (udp_) { - udp_->stop(); + if (this->socket_) { + this->socket_->close(); } } void E131Component::setup() { - udp_ = make_unique(); + this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP); - if (!udp_->begin(PORT)) { - ESP_LOGE(TAG, "Cannot bind E131 to %d.", PORT); - mark_failed(); + int enable = 1; + int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); + // we can still continue + } + err = this->socket_->setblocking(false); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); + this->mark_failed(); + return; + } + + struct sockaddr_storage server; + + socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), PORT); + if (sl == 0) { + ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno); + this->mark_failed(); + return; + } + server.ss_family = AF_INET; + + err = this->socket_->bind((struct sockaddr *) &server, sizeof(server)); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); + this->mark_failed(); return; } @@ -43,22 +56,22 @@ void E131Component::loop() { std::vector payload; E131Packet packet; int universe = 0; + uint8_t buf[1460]; - while (uint16_t packet_size = udp_->parsePacket()) { - payload.resize(packet_size); + ssize_t len = this->socket_->read(buf, sizeof(buf)); + if (len == -1) { + return; + } + payload.resize(len); + memmove(&payload[0], buf, len); - if (!udp_->read(&payload[0], payload.size())) { - continue; - } + if (!this->packet_(payload, universe, packet)) { + ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size()); + return; + } - if (!packet_(payload, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size()); - continue; - } - - if (!process_(universe, packet)) { - ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); - } + if (!this->process_(universe, packet)) { + ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); } } @@ -106,5 +119,3 @@ bool E131Component::process_(int universe, const E131Packet &packet) { } // namespace e131 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index 648cfb4585..364a05af75 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -1,14 +1,12 @@ #pragma once -#ifdef USE_ARDUINO - +#include "esphome/components/socket/socket.h" #include "esphome/core/component.h" +#include #include #include -#include - -class UDP; +#include namespace esphome { namespace e131 { @@ -46,7 +44,7 @@ class E131Component : public esphome::Component { void leave_(int universe); E131ListenMethod listen_method_{E131_MULTICAST}; - std::unique_ptr udp_; + std::unique_ptr socket_; std::set light_effects_; std::map universe_consumers_; std::map universe_packets_; @@ -54,5 +52,3 @@ class E131Component : public esphome::Component { } // namespace e131 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp index 7a3e71808e..42eb0fc56b 100644 --- a/esphome/components/e131/e131_addressable_light_effect.cpp +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -1,7 +1,5 @@ -#ifdef USE_ARDUINO - -#include "e131.h" #include "e131_addressable_light_effect.h" +#include "e131.h" #include "esphome/core/log.h" namespace esphome { @@ -92,5 +90,3 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet } // namespace e131 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/e131/e131_addressable_light_effect.h b/esphome/components/e131/e131_addressable_light_effect.h index b3e481e43b..56df9cd80f 100644 --- a/esphome/components/e131/e131_addressable_light_effect.h +++ b/esphome/components/e131/e131_addressable_light_effect.h @@ -1,7 +1,5 @@ #pragma once -#ifdef USE_ARDUINO - #include "esphome/core/component.h" #include "esphome/components/light/addressable_light_effect.h" @@ -44,5 +42,3 @@ class E131AddressableLightEffect : public light::AddressableLightEffect { } // namespace e131 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index f199d3574b..ac8b72f6e7 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -1,15 +1,13 @@ -#ifdef USE_ARDUINO - +#include #include "e131.h" +#include "esphome/components/network/ip_address.h" #include "esphome/core/log.h" #include "esphome/core/util.h" -#include "esphome/components/network/ip_address.h" -#include -#include -#include -#include #include +#include +#include +#include namespace esphome { namespace e131 { @@ -62,7 +60,7 @@ const size_t E131_MIN_PACKET_SIZE = reinterpret_cast(&((E131RawPacket *) bool E131Component::join_igmp_groups_() { if (listen_method_ != E131_MULTICAST) return false; - if (!udp_) + if (this->socket_ == nullptr) return false; for (auto universe : universe_consumers_) { @@ -140,5 +138,3 @@ bool E131Component::packet_(const std::vector &data, int &universe, E13 } // namespace e131 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/ee895/__init__.py b/esphome/components/ee895/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ee895/ee895.cpp b/esphome/components/ee895/ee895.cpp new file mode 100644 index 0000000000..a7186ffbbc --- /dev/null +++ b/esphome/components/ee895/ee895.cpp @@ -0,0 +1,115 @@ +#include "ee895.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ee895 { + +static const char *const TAG = "ee895"; + +static const uint16_t CRC16_ONEWIRE_START = 0xFFFF; +static const uint8_t FUNCTION_CODE_READ = 0x03; +static const uint16_t SERIAL_NUMBER = 0x0000; +static const uint16_t TEMPERATURE_ADDRESS = 0x03EA; +static const uint16_t CO2_ADDRESS = 0x0424; +static const uint16_t PRESSURE_ADDRESS = 0x04B0; + +void EE895Component::setup() { + uint16_t crc16_check = 0; + ESP_LOGCONFIG(TAG, "Setting up EE895..."); + write_command_(SERIAL_NUMBER, 8); + uint8_t serial_number[20]; + this->read(serial_number, 20); + + crc16_check = (serial_number[19] << 8) + serial_number[18]; + if (crc16_check != calc_crc16_(serial_number, 19)) { + this->error_code_ = CRC_CHECK_FAILED; + this->mark_failed(); + return; + } + ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex(serial_number + 2, 16).c_str()); +} + +void EE895Component::dump_config() { + ESP_LOGCONFIG(TAG, "EE895:"); + LOG_I2C_DEVICE(this); + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Communication with EE895 failed!"); + break; + case CRC_CHECK_FAILED: + ESP_LOGE(TAG, "The crc check failed"); + break; + case NONE: + default: + break; + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); +} + +float EE895Component::get_setup_priority() const { return setup_priority::DATA; } + +void EE895Component::update() { + write_command_(TEMPERATURE_ADDRESS, 2); + this->set_timeout(50, [this]() { + float temperature = read_float_(); + + write_command_(CO2_ADDRESS, 2); + float co2 = read_float_(); + + write_command_(PRESSURE_ADDRESS, 2); + float pressure = read_float_(); + ESP_LOGD(TAG, "Got temperature=%.1f°C co2=%.0fppm pressure=%.1f%mbar", temperature, co2, pressure); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(co2); + if (this->pressure_sensor_ != nullptr) + this->pressure_sensor_->publish_state(pressure); + this->status_clear_warning(); + }); +} + +void EE895Component::write_command_(uint16_t addr, uint16_t reg_cnt) { + uint8_t address[7]; + uint16_t crc16 = 0; + address[0] = FUNCTION_CODE_READ; + address[1] = (addr >> 8) & 0xFF; + address[2] = addr & 0xFF; + address[3] = (reg_cnt >> 8) & 0xFF; + address[4] = reg_cnt & 0xFF; + crc16 = calc_crc16_(address, 6); + address[5] = crc16 & 0xFF; + address[6] = (crc16 >> 8) & 0xFF; + this->write(address, 7, true); +} + +float EE895Component::read_float_() { + uint16_t crc16_check = 0; + uint8_t i2c_response[8]; + this->read(i2c_response, 8); + crc16_check = (i2c_response[7] << 8) + i2c_response[6]; + if (crc16_check != calc_crc16_(i2c_response, 7)) { + this->error_code_ = CRC_CHECK_FAILED; + this->status_set_warning(); + return 0; + } + uint32_t x = encode_uint32(i2c_response[4], i2c_response[5], i2c_response[2], i2c_response[3]); + float value; + memcpy(&value, &x, sizeof(value)); // convert uin32_t IEEE-754 format to float + return value; +} + +uint16_t EE895Component::calc_crc16_(const uint8_t buf[], uint8_t len) { + uint8_t crc_check_buf[22]; + for (int i = 0; i < len; i++) { + crc_check_buf[i + 1] = buf[i]; + } + crc_check_buf[0] = this->address_; + return crc16(crc_check_buf, len); +} +} // namespace ee895 +} // namespace esphome diff --git a/esphome/components/ee895/ee895.h b/esphome/components/ee895/ee895.h new file mode 100644 index 0000000000..83bd7c6e82 --- /dev/null +++ b/esphome/components/ee895/ee895.h @@ -0,0 +1,34 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ee895 { + +/// This class implements support for the ee895 of temperature i2c sensors. +class EE895Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } + + float get_setup_priority() const override; + void setup() override; + void dump_config() override; + void update() override; + + protected: + void write_command_(uint16_t addr, uint16_t reg_cnt); + float read_float_(); + uint16_t calc_crc16_(const uint8_t buf[], uint8_t len); + sensor::Sensor *co2_sensor_; + sensor::Sensor *temperature_sensor_; + sensor::Sensor *pressure_sensor_; + + enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; +}; + +} // namespace ee895 +} // namespace esphome diff --git a/esphome/components/ee895/sensor.py b/esphome/components/ee895/sensor.py new file mode 100644 index 0000000000..d06f9ca02f --- /dev/null +++ b/esphome/components/ee895/sensor.py @@ -0,0 +1,69 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_PRESSURE, + CONF_TEMPERATURE, + CONF_CO2, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + STATE_CLASS_MEASUREMENT, + UNIT_HECTOPASCAL, + UNIT_CELSIUS, + ICON_MOLECULE_CO2, + UNIT_PARTS_PER_MILLION, +) + +CODEOWNERS = ["@Stock-M"] + +DEPENDENCIES = ["i2c"] + +ee895_ns = cg.esphome_ns.namespace("ee895") +EE895Component = ee895_ns.class_("EE895Component", cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(EE895Component), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_CO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x5F)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_CO2 in config: + sens = await sensor.new_sensor(config[CONF_CO2]) + cg.add(var.set_co2_sensor(sens)) + + if CONF_PRESSURE in config: + sens = await sensor.new_sensor(config[CONF_PRESSURE]) + cg.add(var.set_pressure_sensor(sens)) diff --git a/esphome/components/ektf2232/ektf2232.cpp b/esphome/components/ektf2232/ektf2232.cpp index 8df25fce24..80f5f8a8e2 100644 --- a/esphome/components/ektf2232/ektf2232.cpp +++ b/esphome/components/ektf2232/ektf2232.cpp @@ -2,6 +2,8 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace ektf2232 { diff --git a/esphome/components/ektf2232/touchscreen.py b/esphome/components/ektf2232/touchscreen.py index b3513b2670..d937265e7a 100644 --- a/esphome/components/ektf2232/touchscreen.py +++ b/esphome/components/ektf2232/touchscreen.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import i2c, touchscreen -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["i2c"] @@ -17,10 +17,8 @@ EKTF2232Touchscreen = ektf2232_ns.class_( ) CONF_EKTF2232_ID = "ektf2232_id" -CONF_INTERRUPT_PIN = "interrupt_pin" CONF_RTS_PIN = "rts_pin" - CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( cv.Schema( { diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index 67c6a4ebd3..1190acc46b 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -11,7 +11,9 @@ using namespace esphome::cover; CoverTraits EndstopCover::get_traits() { auto traits = CoverTraits(); + traits.set_supports_stop(true); traits.set_supports_position(true); + traits.set_supports_toggle(true); traits.set_is_assumed_state(false); return traits; } @@ -20,6 +22,20 @@ void EndstopCover::control(const CoverCall &call) { this->start_direction_(COVER_OPERATION_IDLE); this->publish_state(); } + if (call.get_toggle().has_value()) { + if (this->current_operation != COVER_OPERATION_IDLE) { + this->start_direction_(COVER_OPERATION_IDLE); + this->publish_state(); + } else { + if (this->position == COVER_CLOSED || this->last_operation_ == COVER_OPERATION_CLOSING) { + this->target_position_ = COVER_OPEN; + this->start_direction_(COVER_OPERATION_OPENING); + } else { + this->target_position_ = COVER_CLOSED; + this->start_direction_(COVER_OPERATION_CLOSING); + } + } + } if (call.get_position().has_value()) { auto pos = *call.get_position(); if (pos == this->position) { @@ -125,9 +141,11 @@ void EndstopCover::start_direction_(CoverOperation dir) { trig = this->stop_trigger_; break; case COVER_OPERATION_OPENING: + this->last_operation_ = dir; trig = this->open_trigger_; break; case COVER_OPERATION_CLOSING: + this->last_operation_ = dir; trig = this->close_trigger_; break; default: diff --git a/esphome/components/endstop/endstop_cover.h b/esphome/components/endstop/endstop_cover.h index f8d2746234..6ae15de8c1 100644 --- a/esphome/components/endstop/endstop_cover.h +++ b/esphome/components/endstop/endstop_cover.h @@ -51,6 +51,7 @@ class EndstopCover : public cover::Cover, public Component { uint32_t start_dir_time_{0}; uint32_t last_publish_time_{0}; float target_position_{0}; + cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING}; }; } // namespace endstop diff --git a/esphome/components/ens210/__init__.py b/esphome/components/ens210/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ens210/ens210.cpp b/esphome/components/ens210/ens210.cpp new file mode 100644 index 0000000000..86890c05e8 --- /dev/null +++ b/esphome/components/ens210/ens210.cpp @@ -0,0 +1,230 @@ +// ENS210 relative humidity and temperature sensor with I2C interface from ScioSense +// +// Datasheet: https://www.sciosense.com/wp-content/uploads/2021/01/ENS210.pdf +// +// Implementation based on: +// https://github.com/maarten-pennings/ENS210 +// https://github.com/sciosense/ENS210_driver + +#include "ens210.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace ens210 { + +static const char *const TAG = "ens210"; + +// ENS210 chip constants +static const uint8_t ENS210_BOOTING_MS = 2; // Booting time in ms (also after reset, or going to high power) +static const uint8_t ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS = + 130; // Conversion time in ms for single shot T/H measurement +static const uint16_t ENS210_PART_ID = 0x0210; // The expected part id of the ENS210 + +// Addresses of the ENS210 registers +static const uint8_t ENS210_REGISTER_PART_ID = 0x00; +static const uint8_t ENS210_REGISTER_UID = 0x04; +static const uint8_t ENS210_REGISTER_SYS_CTRL = 0x10; +static const uint8_t ENS210_REGISTER_SYS_STAT = 0x11; +static const uint8_t ENS210_REGISTER_SENS_RUN = 0x21; +static const uint8_t ENS210_REGISTER_SENS_START = 0x22; +static const uint8_t ENS210_REGISTER_SENS_STOP = 0x23; +static const uint8_t ENS210_REGISTER_SENS_STAT = 0x24; +static const uint8_t ENS210_REGISTER_T_VAL = 0x30; +static const uint8_t ENS210_REGISTER_H_VAL = 0x33; + +// CRC-7 constants +static const uint8_t CRC7_WIDTH = 7; // A 7 bits CRC has polynomial of 7th order, which has 8 terms +static const uint8_t CRC7_POLY = 0x89; // The 8 coefficients of the polynomial +static const uint8_t CRC7_IVEC = 0x7F; // Initial vector has all 7 bits high + +// Payload data constants +static const uint8_t DATA7_WIDTH = 17; +static const uint32_t DATA7_MASK = ((1UL << DATA7_WIDTH) - 1); // 0b 0 1111 1111 1111 1111 +static const uint32_t DATA7_MSB = (1UL << (DATA7_WIDTH - 1)); // 0b 1 0000 0000 0000 0000 + +// Converts a status to a human readable string +static const LogString *ens210_status_to_human(int status) { + switch (status) { + case ENS210Component::ENS210_STATUS_I2C_ERROR: + return LOG_STR("I2C error - communication with ENS210 failed!"); + case ENS210Component::ENS210_STATUS_CRC_ERROR: + return LOG_STR("CRC error"); + case ENS210Component::ENS210_STATUS_INVALID: + return LOG_STR("Invalid data"); + case ENS210Component::ENS210_STATUS_OK: + return LOG_STR("Status OK"); + case ENS210Component::ENS210_WRONG_CHIP_ID: + return LOG_STR("ENS210 has wrong chip ID! Is it a ENS210?"); + default: + return LOG_STR("Unknown"); + } +} + +// Compute the CRC-7 of 'value' (should only have 17 bits) +// https://en.wikipedia.org/wiki/Cyclic_redundancy_check#Computation +static uint32_t crc7(uint32_t value) { + // Setup polynomial + uint32_t polynomial = CRC7_POLY; + // Align polynomial with data + polynomial = polynomial << (DATA7_WIDTH - CRC7_WIDTH - 1); + // Loop variable (indicates which bit to test, start with highest) + uint32_t bit = DATA7_MSB; + // Make room for CRC value + value = value << CRC7_WIDTH; + bit = bit << CRC7_WIDTH; + polynomial = polynomial << CRC7_WIDTH; + // Insert initial vector + value |= CRC7_IVEC; + // Apply division until all bits done + while (bit & (DATA7_MASK << CRC7_WIDTH)) { + if (bit & value) + value ^= polynomial; + bit >>= 1; + polynomial >>= 1; + } + return value; +} + +void ENS210Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up ENS210..."); + uint8_t data[2]; + uint16_t part_id = 0; + // Reset + if (!this->write_byte(ENS210_REGISTER_SYS_CTRL, 0x80)) { + this->write_byte(ENS210_REGISTER_SYS_CTRL, 0x80); + this->error_code_ = ENS210_STATUS_I2C_ERROR; + this->mark_failed(); + return; + } + // Wait to boot after reset + delay(ENS210_BOOTING_MS); + // Must disable low power to read PART_ID + if (!set_low_power_(false)) { + // Try to go back to default mode (low power enabled) + set_low_power_(true); + this->error_code_ = ENS210_STATUS_I2C_ERROR; + this->mark_failed(); + return; + } + // Read the PART_ID + if (!this->read_bytes(ENS210_REGISTER_PART_ID, data, 2)) { + // Try to go back to default mode (low power enabled) + set_low_power_(true); + this->error_code_ = ENS210_STATUS_I2C_ERROR; + this->mark_failed(); + return; + } + // Pack bytes into partid + part_id = data[1] * 256U + data[0] * 1U; + // Check expected part id of the ENS210 + if (part_id != ENS210_PART_ID) { + this->error_code_ = ENS210_WRONG_CHIP_ID; + this->mark_failed(); + } + // Set default power mode (low power enabled) + set_low_power_(true); +} + +void ENS210Component::dump_config() { + ESP_LOGCONFIG(TAG, "ENS210:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "%s", LOG_STR_ARG(ens210_status_to_human(this->error_code_))); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +float ENS210Component::get_setup_priority() const { return setup_priority::DATA; } + +void ENS210Component::update() { + // Execute a single measurement + if (!this->write_byte(ENS210_REGISTER_SENS_RUN, 0x00)) { + ESP_LOGE(TAG, "Starting single measurement failed!"); + this->status_set_warning(); + return; + } + // Trigger measurement + if (!this->write_byte(ENS210_REGISTER_SENS_START, 0x03)) { + ESP_LOGE(TAG, "Trigger of measurement failed!"); + this->status_set_warning(); + return; + } + // Wait for measurement to complete + this->set_timeout("data", uint32_t(ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS), [this]() { + int temperature_data, temperature_status, humidity_data, humidity_status; + uint8_t data[6]; + uint32_t h_val_data, t_val_data; + // Set default status for early bail out + temperature_status = ENS210_STATUS_I2C_ERROR; + humidity_status = ENS210_STATUS_I2C_ERROR; + + // Read T_VAL and H_VAL + if (!this->read_bytes(ENS210_REGISTER_T_VAL, data, 6)) { + ESP_LOGE(TAG, "Communication with ENS210 failed!"); + this->status_set_warning(); + return; + } + // Pack bytes for humidity + h_val_data = (uint32_t) ((uint32_t) data[5] << 16 | (uint32_t) data[4] << 8 | (uint32_t) data[3]); + // Extract humidity data and update the status + extract_measurement_(h_val_data, &humidity_data, &humidity_status); + + if (humidity_status == ENS210_STATUS_OK) { + if (this->humidity_sensor_ != nullptr) { + float humidity = (humidity_data & 0xFFFF) / 512.0; + this->humidity_sensor_->publish_state(humidity); + } + } else { + ESP_LOGW(TAG, "Humidity status failure: %s", LOG_STR_ARG(ens210_status_to_human(humidity_status))); + this->status_set_warning(); + return; + } + // Pack bytes for temperature + t_val_data = (uint32_t) ((uint32_t) data[2] << 16 | (uint32_t) data[1] << 8 | (uint32_t) data[0]); + // Extract temperature data and update the status + extract_measurement_(t_val_data, &temperature_data, &temperature_status); + + if (temperature_status == ENS210_STATUS_OK) { + if (this->temperature_sensor_ != nullptr) { + // Temperature in Celsius + float temperature = (temperature_data & 0xFFFF) / 64.0 - 27315L / 100.0; + this->temperature_sensor_->publish_state(temperature); + } + } else { + ESP_LOGW(TAG, "Temperature status failure: %s", LOG_STR_ARG(ens210_status_to_human(temperature_status))); + } + }); +} + +// Extracts measurement 'data' and 'status' from a 'val' obtained from measurement. +void ENS210Component::extract_measurement_(uint32_t val, int *data, int *status) { + *data = (val >> 0) & 0xffff; + int valid = (val >> 16) & 0x1; + uint32_t crc = (val >> 17) & 0x7f; + uint32_t payload = (val >> 0) & 0x1ffff; + // Check CRC + uint8_t crc_ok = crc7(payload) == crc; + + if (!crc_ok) { + *status = ENS210_STATUS_CRC_ERROR; + } else if (!valid) { + *status = ENS210_STATUS_INVALID; + } else { + *status = ENS210_STATUS_OK; + } +} + +// Sets ENS210 to low (true) or high (false) power. Returns false on I2C problems. +bool ENS210Component::set_low_power_(bool enable) { + uint8_t low_power_cmd = enable ? 0x01 : 0x00; + ESP_LOGD(TAG, "Enable low power: %s", enable ? "true" : "false"); + bool result = this->write_byte(ENS210_REGISTER_SYS_CTRL, low_power_cmd); + delay(ENS210_BOOTING_MS); + return result; +} + +} // namespace ens210 +} // namespace esphome diff --git a/esphome/components/ens210/ens210.h b/esphome/components/ens210/ens210.h new file mode 100644 index 0000000000..0fb6ff634d --- /dev/null +++ b/esphome/components/ens210/ens210.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ens210 { + +/// This class implements support for the ENS210 relative humidity and temperature i2c sensor. +class ENS210Component : public PollingComponent, public i2c::I2CDevice { + public: + float get_setup_priority() const override; + void dump_config() override; + void setup() override; + void update() override; + + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + + enum ErrorCode { + ENS210_STATUS_OK = 0, // The value was read, the CRC matches, and data is valid + ENS210_STATUS_INVALID, // The value was read, the CRC matches, but the data is invalid (e.g. the measurement was + // not yet finished) + ENS210_STATUS_CRC_ERROR, // The value was read, but the CRC over the payload (valid and data) does not match + ENS210_STATUS_I2C_ERROR, // There was an I2C communication error + ENS210_WRONG_CHIP_ID // The read PART_ID is not the expected part id of the ENS210 + } error_code_{ENS210_STATUS_OK}; + + protected: + bool set_low_power_(bool enable); + void extract_measurement_(uint32_t val, int *data, int *status); + + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; +}; + +} // namespace ens210 +} // namespace esphome diff --git a/esphome/components/ens210/sensor.py b/esphome/components/ens210/sensor.py new file mode 100644 index 0000000000..3037156e01 --- /dev/null +++ b/esphome/components/ens210/sensor.py @@ -0,0 +1,58 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +CODEOWNERS = ["@itn3rd77"] +DEPENDENCIES = ["i2c"] + +ens210_ns = cg.esphome_ns.namespace("ens210") + +ENS210Component = ens210_ns.class_( + "ENS210Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ENS210Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x43)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_HUMIDITY in config: + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 1229675ad8..3ca140f0d4 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -4,36 +4,50 @@ from pathlib import Path import logging import os -from esphome.helpers import copy_file_if_changed, write_file_if_changed +from esphome.helpers import copy_file_if_changed, write_file_if_changed, mkdir_p from esphome.const import ( CONF_BOARD, + CONF_COMPONENTS, CONF_FRAMEWORK, + CONF_NAME, CONF_SOURCE, CONF_TYPE, CONF_VARIANT, CONF_VERSION, CONF_ADVANCED, + CONF_REFRESH, + CONF_PATH, + CONF_URL, + CONF_REF, CONF_IGNORE_EFUSE_MAC_CRC, KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, + TYPE_GIT, + TYPE_LOCAL, __version__, ) -from esphome.core import CORE, HexInt +from esphome.core import CORE, HexInt, TimePeriod import esphome.config_validation as cv import esphome.codegen as cg +from esphome import git from .const import ( # noqa KEY_BOARD, + KEY_COMPONENTS, KEY_ESP32, + KEY_PATH, + KEY_REF, + KEY_REFRESH, + KEY_REPO, KEY_SDKCONFIG_OPTIONS, KEY_VARIANT, VARIANT_ESP32C3, VARIANT_FRIENDLY, VARIANTS, ) -from .boards import BOARD_TO_VARIANT +from .boards import BOARDS # force import gpio to register pin schema from .gpio import esp32_pin_to_code # noqa @@ -51,6 +65,7 @@ def set_core_data(config): if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "esp-idf" CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {} + CORE.data[KEY_ESP32][KEY_COMPONENTS] = {} elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( @@ -61,12 +76,30 @@ def set_core_data(config): return config -def get_esp32_variant(): - return CORE.data[KEY_ESP32][KEY_VARIANT] +def get_esp32_variant(core_obj=None): + return (core_obj or CORE).data[KEY_ESP32][KEY_VARIANT] -def is_esp32c3(): - return get_esp32_variant() == VARIANT_ESP32C3 +def only_on_variant(*, supported=None, unsupported=None): + """Config validator for features only available on some ESP32 variants.""" + if supported is not None and not isinstance(supported, list): + supported = [supported] + if unsupported is not None and not isinstance(unsupported, list): + unsupported = [unsupported] + + def validator_(obj): + variant = get_esp32_variant() + if supported is not None and variant not in supported: + raise cv.Invalid( + f"This feature is only available on {', '.join(supported)}" + ) + if unsupported is not None and variant in unsupported: + raise cv.Invalid( + f"This feature is not available on {', '.join(unsupported)}" + ) + return obj + + return validator_ @dataclass @@ -86,6 +119,21 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType): CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS][name] = value +def add_idf_component( + name: str, repo: str, ref: str = None, path: str = None, refresh: TimePeriod = None +): + """Add an esp-idf component to the project.""" + if not CORE.using_esp_idf: + raise ValueError("Not an esp-idf project") + if name not in CORE.data[KEY_ESP32][KEY_COMPONENTS]: + CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = { + KEY_REPO: repo, + KEY_REF: ref, + KEY_PATH: path, + KEY_REFRESH: refresh, + } + + def _format_framework_arduino_version(ver: cv.Version) -> str: # format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to # a PIO platformio/framework-arduinoespressif32 value @@ -111,27 +159,27 @@ def _format_framework_espidf_version(ver: cv.Version) -> str: # The default/recommended arduino framework version # - https://github.com/espressif/arduino-esp32/releases # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif32 -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(1, 0, 6) +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(2, 0, 5) # The platformio/espressif32 version to use for arduino frameworks # - https://github.com/platformio/platform-espressif32/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 -ARDUINO_PLATFORM_VERSION = cv.Version(3, 5, 0) +ARDUINO_PLATFORM_VERSION = cv.Version(5, 3, 0) # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf -RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 3, 2) +RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 4) # The platformio/espressif32 version to use for esp-idf frameworks # - https://github.com/platformio/platform-espressif32/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 -ESP_IDF_PLATFORM_VERSION = cv.Version(3, 5, 0) +ESP_IDF_PLATFORM_VERSION = cv.Version(5, 3, 0) def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(2, 0, 0), "https://github.com/espressif/arduino-esp32.git"), - "latest": (cv.Version(1, 0, 6), None), + "dev": (cv.Version(2, 1, 0), "https://github.com/espressif/arduino-esp32.git"), + "latest": (cv.Version(2, 0, 7), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } @@ -165,8 +213,8 @@ def _arduino_check_versions(value): def _esp_idf_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(5, 0, 0), "https://github.com/espressif/esp-idf.git"), - "latest": (cv.Version(4, 3, 2), None), + "dev": (cv.Version(5, 1, 0), "https://github.com/espressif/esp-idf.git"), + "latest": (cv.Version(5, 0, 1), None), "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), } @@ -204,7 +252,7 @@ def _parse_platform_version(value): try: # if platform version is a valid version constraint, prefix the default package cv.platformio_version_constraint(value) - return f"platformio/espressif32 @ {value}" + return f"platformio/espressif32@{value}" except cv.Invalid: return value @@ -212,14 +260,14 @@ def _parse_platform_version(value): def _detect_variant(value): if CONF_VARIANT not in value: board = value[CONF_BOARD] - if board not in BOARD_TO_VARIANT: + if board not in BOARDS: raise cv.Invalid( "This board is unknown, please set the variant manually", path=[CONF_BOARD], ) value = value.copy() - value[CONF_VARIANT] = BOARD_TO_VARIANT[board] + value[CONF_VARIANT] = BOARDS[board][KEY_VARIANT] return value @@ -252,6 +300,18 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean, } ), + cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( + cv.Schema( + { + cv.Required(CONF_NAME): cv.string_strict, + cv.Required(CONF_SOURCE): cv.SOURCE_SCHEMA, + cv.Optional(CONF_PATH): cv.string, + cv.Optional(CONF_REFRESH, default="1d"): cv.All( + cv.string, cv.source_refresh + ), + } + ) + ), } ), _esp_idf_check_versions, @@ -307,7 +367,12 @@ async def to_code(config): cg.add_build_flag("-Wno-nonnull-compare") cg.add_platformio_option( "platform_packages", - [f"platformio/framework-espidf @ {conf[CONF_SOURCE]}"], + [f"platformio/framework-espidf@{conf[CONF_SOURCE]}"], + ) + # platformio/toolchain-esp32ulp does not support linux_aarch64 yet and has not been updated for over 2 years + # This is espressif's own published version which is more up to date. + cg.add_platformio_option( + "platform_packages", ["espressif/toolchain-esp32ulp@2.35.0-20220830"] ) add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False) add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True) @@ -333,9 +398,14 @@ async def to_code(config): if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_MAC_CRC]: cg.add_define("USE_ESP32_IGNORE_EFUSE_MAC_CRC") - add_idf_sdkconfig_option( - "CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE", False - ) + if (framework_ver.major, framework_ver.minor) >= (4, 4): + add_idf_sdkconfig_option( + "CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False + ) + else: + add_idf_sdkconfig_option( + "CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE", False + ) cg.add_define( "USE_ESP_IDF_VERSION_CODE", @@ -344,13 +414,26 @@ async def to_code(config): ), ) + for component in conf[CONF_COMPONENTS]: + source = component[CONF_SOURCE] + if source[CONF_TYPE] == TYPE_GIT: + add_idf_component( + name=component[CONF_NAME], + repo=source[CONF_URL], + ref=source.get(CONF_REF), + path=component.get(CONF_PATH), + refresh=component[CONF_REFRESH], + ) + elif source[CONF_TYPE] == TYPE_LOCAL: + _LOGGER.warning("Local components are not implemented yet.") + elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: cg.add_platformio_option("framework", "arduino") cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") cg.add_platformio_option( "platform_packages", - [f"platformio/framework-arduinoespressif32 @ {conf[CONF_SOURCE]}"], + [f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"], ) cg.add_platformio_option("board_build.partitions", "partitions.csv") @@ -375,11 +458,11 @@ spiffs, data, spiffs, 0x391000, 0x00F000 IDF_PARTITIONS_CSV = """\ # Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, , 0x4000, otadata, data, ota, , 0x2000, phy_init, data, phy, , 0x1000, app0, app, ota_0, , 0x1C0000, app1, app, ota_1, , 0x1C0000, +nvs, data, nvs, , 0x6d000, """ @@ -440,6 +523,32 @@ def copy_files(): __version__, ) + import shutil + + shutil.rmtree(CORE.relative_build_path("components"), ignore_errors=True) + + if CORE.data[KEY_ESP32][KEY_COMPONENTS]: + components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] + + for name, component in components.items(): + repo_dir, _ = git.clone_or_update( + url=component[KEY_REPO], + ref=component[KEY_REF], + refresh=component[KEY_REFRESH], + domain="idf_components", + ) + mkdir_p(CORE.relative_build_path("components")) + component_dir = repo_dir + if component[KEY_PATH] is not None: + component_dir = component_dir / component[KEY_PATH] + + shutil.copytree( + component_dir, + CORE.relative_build_path(f"components/{name}"), + dirs_exist_ok=True, + ignore=shutil.ignore_patterns(".git", ".github"), + ) + dir = os.path.dirname(__file__) post_build_file = os.path.join(dir, "post_build.py.script") copy_file_if_changed( diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index 56fd4932b4..30297654bc 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -1,4 +1,4 @@ -from .const import VARIANT_ESP32, VARIANT_ESP32S2, VARIANT_ESP32C3 +from .const import VARIANT_ESP32, VARIANT_ESP32S2, VARIANT_ESP32C3, VARIANT_ESP32S3 ESP32_BASE_PINS = { "TX": 1, @@ -42,6 +42,97 @@ ESP32_BASE_PINS = { } ESP32_BOARD_PINS = { + "adafruit_feather_esp32s2_tft": { + "BUTTON": 0, + "A0": 18, + "A1": 17, + "A2": 16, + "A3": 15, + "A4": 14, + "A5": 8, + "SCK": 36, + "MOSI": 35, + "MISO": 37, + "RX": 2, + "TX": 1, + "D13": 13, + "D12": 12, + "D11": 11, + "D10": 10, + "D9": 9, + "D6": 6, + "D5": 5, + "NEOPIXEL": 33, + "PIN_NEOPIXEL": 33, + "NEOPIXEL_POWER": 34, + "SCL": 41, + "SDA": 42, + "TFT_I2C_POWER": 21, + "TFT_CS": 7, + "TFT_DC": 39, + "TFT_RESET": 40, + "TFT_BACKLIGHT": 45, + "LED": 13, + "LED_BUILTIN": 13, + }, + "adafruit_qtpy_esp32c3": { + "A0": 4, + "A1": 3, + "A2": 1, + "A3": 0, + "SDA": 5, + "SCL": 6, + "MOSI": 7, + "MISO": 8, + "SCK": 10, + "RX": 20, + "TX": 21, + "NEOPIXEL": 2, + "PIN_NEOPIXEL": 2, + "BUTTON": 9, + "SWITCH": 9, + }, + "adafruit_qtpy_esp32s2": { + "A0": 18, + "A1": 17, + "A2": 9, + "A3": 8, + "SDA": 7, + "SCL": 6, + "MOSI": 35, + "MISO": 37, + "SCK": 36, + "RX": 16, + "TX": 5, + "SDA1": 41, + "SCL1": 40, + "NEOPIXEL": 39, + "PIN_NEOPIXEL": 39, + "NEOPIXEL_POWER": 38, + "D0": 0, + "BUTTON": 0, + "SWITCH": 0, + }, + "adafruit_qtpy_esp32": { + "A0": 26, + "A1": 25, + "A2": 27, + "A3": 15, + "SDA": 4, + "SCL": 33, + "MOSI": 13, + "MISO": 12, + "SCK": 14, + "RX": 7, + "TX": 32, + "SDA1": 22, + "SCL1": 19, + "NEOPIXEL": 5, + "PIN_NEOPIXEL": 5, + "NEOPIXEL_POWER": 8, + "BUTTON": 0, + "SWITCH": 0, + }, "alksesp32": { "A0": 32, "A1": 33, @@ -521,8 +612,54 @@ ESP32_BOARD_PINS = { }, "lolin32": {"LED": 5}, "lolin32_lite": {"LED": 22}, + "lolin_c3_mini": { + "TX": 21, + "RX": 20, + "SDA": 8, + "SCL": 10, + "SS": 5, + "MOSI": 4, + "MISO": 3, + "SCK": 2, + "A0": 0, + "A1": 1, + "A2": 2, + "A3": 3, + "A4": 4, + "A5": 5, + "D0": 1, + "D1": 10, + "D2": 8, + "D3": 7, + "D4": 6, + "D5": 2, + "D6": 3, + "D7": 4, + "D8": 5, + "LED": 7, + "BUTTON": 9, + }, "lolin_d32": {"LED": 5, "_VBAT": 35}, "lolin_d32_pro": {"LED": 5, "_VBAT": 35}, + "lolin_s2_mini": { + "TX": 43, + "RX": 44, + "SPICS1": 29, + "SPIHD": 31, + "SPIWP": 32, + "SPICS0": 33, + "SPICLK": 34, + "SPIQ": 35, + "SPID": 36, + "MISO": 9, + "MOSI": 11, + "SCK": 7, + "SCL": 35, + "SDA": 33, + "DAC1": 17, + "DAC2": 18, + "LED": 15, + }, "lopy": { "A1": 37, "A2": 38, @@ -957,126 +1094,743 @@ ESP32_BOARD_PINS = { "D13": 2, }, "xinabox_cw02": {"LED": 27}, + "upesy_wroom": {"LED": 2}, + "upesy_wrover": {"LED": 2}, } """ -BOARD_TO_VARIANT generated with: +BOARDS generated with: git clone https://github.com/platformio/platform-espressif32 for x in platform-espressif32/boards/*.json; do mcu=$(jq -r .build.mcu <"$x"); + name=$(jq -r .name <"$x"); fname=$(basename "$x") board="${fname%.*}" variant=$(echo "$mcu" | tr '[:lower:]' '[:upper:]') - echo " \"$board\": VARIANT_${variant}," + echo " \"$board\": {\"name\": \"$name\", \"variant\": VARIANT_${variant},}," done | sort """ -BOARD_TO_VARIANT = { - "alksesp32": VARIANT_ESP32, - "az-delivery-devkit-v4": VARIANT_ESP32, - "bpi-bit": VARIANT_ESP32, - "briki_abc_esp32": VARIANT_ESP32, - "briki_mbc-wb_esp32": VARIANT_ESP32, - "d-duino-32": VARIANT_ESP32, - "esp320": VARIANT_ESP32, - "esp32-c3-devkitm-1": VARIANT_ESP32C3, - "esp32cam": VARIANT_ESP32, - "esp32-devkitlipo": VARIANT_ESP32, - "esp32dev": VARIANT_ESP32, - "esp32doit-devkit-v1": VARIANT_ESP32, - "esp32doit-espduino": VARIANT_ESP32, - "esp32-evb": VARIANT_ESP32, - "esp32-gateway": VARIANT_ESP32, - "esp32-poe-iso": VARIANT_ESP32, - "esp32-poe": VARIANT_ESP32, - "esp32-pro": VARIANT_ESP32, - "esp32-s2-kaluga-1": VARIANT_ESP32S2, - "esp32-s2-saola-1": VARIANT_ESP32S2, - "esp32thing_plus": VARIANT_ESP32, - "esp32thing": VARIANT_ESP32, - "esp32vn-iot-uno": VARIANT_ESP32, - "espea32": VARIANT_ESP32, - "espectro32": VARIANT_ESP32, - "espino32": VARIANT_ESP32, - "esp-wrover-kit": VARIANT_ESP32, - "etboard": VARIANT_ESP32, - "featheresp32-s2": VARIANT_ESP32S2, - "featheresp32": VARIANT_ESP32, - "firebeetle32": VARIANT_ESP32, - "fm-devkit": VARIANT_ESP32, - "frogboard": VARIANT_ESP32, - "healthypi4": VARIANT_ESP32, - "heltec_wifi_kit_32_v2": VARIANT_ESP32, - "heltec_wifi_kit_32": VARIANT_ESP32, - "heltec_wifi_lora_32_V2": VARIANT_ESP32, - "heltec_wifi_lora_32": VARIANT_ESP32, - "heltec_wireless_stick_lite": VARIANT_ESP32, - "heltec_wireless_stick": VARIANT_ESP32, - "honeylemon": VARIANT_ESP32, - "hornbill32dev": VARIANT_ESP32, - "hornbill32minima": VARIANT_ESP32, - "imbrios-logsens-v1p1": VARIANT_ESP32, - "inex_openkb": VARIANT_ESP32, - "intorobot": VARIANT_ESP32, - "iotaap_magnolia": VARIANT_ESP32, - "iotbusio": VARIANT_ESP32, - "iotbusproteus": VARIANT_ESP32, - "kits-edu": VARIANT_ESP32, - "labplus_mpython": VARIANT_ESP32, - "lolin32_lite": VARIANT_ESP32, - "lolin32": VARIANT_ESP32, - "lolin_d32_pro": VARIANT_ESP32, - "lolin_d32": VARIANT_ESP32, - "lopy4": VARIANT_ESP32, - "lopy": VARIANT_ESP32, - "m5stack-atom": VARIANT_ESP32, - "m5stack-core2": VARIANT_ESP32, - "m5stack-core-esp32": VARIANT_ESP32, - "m5stack-coreink": VARIANT_ESP32, - "m5stack-fire": VARIANT_ESP32, - "m5stack-grey": VARIANT_ESP32, - "m5stack-timer-cam": VARIANT_ESP32, - "m5stick-c": VARIANT_ESP32, - "magicbit": VARIANT_ESP32, - "mgbot-iotik32a": VARIANT_ESP32, - "mgbot-iotik32b": VARIANT_ESP32, - "mhetesp32devkit": VARIANT_ESP32, - "mhetesp32minikit": VARIANT_ESP32, - "microduino-core-esp32": VARIANT_ESP32, - "nano32": VARIANT_ESP32, - "nina_w10": VARIANT_ESP32, - "node32s": VARIANT_ESP32, - "nodemcu-32s": VARIANT_ESP32, - "nscreen-32": VARIANT_ESP32, - "odroid_esp32": VARIANT_ESP32, - "onehorse32dev": VARIANT_ESP32, - "oroca_edubot": VARIANT_ESP32, - "pico32": VARIANT_ESP32, - "piranha_esp32": VARIANT_ESP32, - "pocket_32": VARIANT_ESP32, - "pycom_gpy": VARIANT_ESP32, - "qchip": VARIANT_ESP32, - "quantum": VARIANT_ESP32, - "sensesiot_weizen": VARIANT_ESP32, - "sg-o_airMon": VARIANT_ESP32, - "s_odi_ultra": VARIANT_ESP32, - "sparkfun_lora_gateway_1-channel": VARIANT_ESP32, - "tinypico": VARIANT_ESP32, - "ttgo-lora32-v1": VARIANT_ESP32, - "ttgo-lora32-v21": VARIANT_ESP32, - "ttgo-lora32-v2": VARIANT_ESP32, - "ttgo-t1": VARIANT_ESP32, - "ttgo-t7-v13-mini32": VARIANT_ESP32, - "ttgo-t7-v14-mini32": VARIANT_ESP32, - "ttgo-t-beam": VARIANT_ESP32, - "ttgo-t-watch": VARIANT_ESP32, - "turta_iot_node": VARIANT_ESP32, - "vintlabs-devkit-v1": VARIANT_ESP32, - "wemosbat": VARIANT_ESP32, - "wemos_d1_mini32": VARIANT_ESP32, - "wesp32": VARIANT_ESP32, - "widora-air": VARIANT_ESP32, - "wifiduino32": VARIANT_ESP32, - "xinabox_cw02": VARIANT_ESP32, +BOARDS = { + "adafruit_feather_esp32s2_tft": { + "name": "Adafruit Feather ESP32-S2 TFT", + "variant": VARIANT_ESP32S2, + }, + "adafruit_feather_esp32s3": { + "name": "Adafruit Feather ESP32-S3 2MB PSRAM", + "variant": VARIANT_ESP32S3, + }, + "adafruit_feather_esp32s3_nopsram": { + "name": "Adafruit Feather ESP32-S3 No PSRAM", + "variant": VARIANT_ESP32S3, + }, + "adafruit_feather_esp32s3_tft": { + "name": "Adafruit Feather ESP32-S3 TFT", + "variant": VARIANT_ESP32S3, + }, + "adafruit_feather_esp32_v2": { + "name": "Adafruit Feather ESP32 V2", + "variant": VARIANT_ESP32, + }, + "adafruit_funhouse_esp32s2": { + "name": "Adafruit FunHouse", + "variant": VARIANT_ESP32S2, + }, + "adafruit_itsybitsy_esp32": { + "name": "Adafruit ItsyBitsy ESP32", + "variant": VARIANT_ESP32, + }, + "adafruit_magtag29_esp32s2": { + "name": "Adafruit MagTag 2.9", + "variant": VARIANT_ESP32S2, + }, + "adafruit_metro_esp32s2": { + "name": "Adafruit Metro ESP32-S2", + "variant": VARIANT_ESP32S2, + }, + "adafruit_qtpy_esp32c3": { + "name": "Adafruit QT Py ESP32-C3", + "variant": VARIANT_ESP32C3, + }, + "adafruit_qtpy_esp32": { + "name": "Adafruit QT Py ESP32", + "variant": VARIANT_ESP32, + }, + "adafruit_qtpy_esp32s2": { + "name": "Adafruit QT Py ESP32-S2", + "variant": VARIANT_ESP32S2, + }, + "adafruit_qtpy_esp32s3_nopsram": { + "name": "Adafruit QT Py ESP32-S3 No PSRAM", + "variant": VARIANT_ESP32S3, + }, + "airm2m_core_esp32c3": { + "name": "AirM2M CORE ESP32C3", + "variant": VARIANT_ESP32C3, + }, + "alksesp32": { + "name": "ALKS ESP32", + "variant": VARIANT_ESP32, + }, + "atmegazero_esp32s2": { + "name": "EspinalLab ATMegaZero ESP32-S2", + "variant": VARIANT_ESP32S2, + }, + "az-delivery-devkit-v4": { + "name": "AZ-Delivery ESP-32 Dev Kit C V4", + "variant": VARIANT_ESP32, + }, + "bee_motion_mini": { + "name": "Smart Bee Motion Mini", + "variant": VARIANT_ESP32C3, + }, + "bee_motion": { + "name": "Smart Bee Motion", + "variant": VARIANT_ESP32S2, + }, + "bee_motion_s3": { + "name": "Smart Bee Motion S3", + "variant": VARIANT_ESP32S3, + }, + "bee_s3": { + "name": "Smart Bee S3", + "variant": VARIANT_ESP32S3, + }, + "bpi-bit": { + "name": "BPI-Bit", + "variant": VARIANT_ESP32, + }, + "briki_abc_esp32": { + "name": "Briki ABC (MBC-WB) - ESP32", + "variant": VARIANT_ESP32, + }, + "briki_mbc-wb_esp32": { + "name": "Briki MBC-WB - ESP32", + "variant": VARIANT_ESP32, + }, + "cnrs_aw2eth": { + "name": "CNRS AW2ETH", + "variant": VARIANT_ESP32, + }, + "connaxio_espoir": { + "name": "Connaxio's Espoir", + "variant": VARIANT_ESP32, + }, + "d-duino-32": { + "name": "D-duino-32", + "variant": VARIANT_ESP32, + }, + "deneyapkart1A": { + "name": "Deneyap Kart 1A", + "variant": VARIANT_ESP32, + }, + "deneyapkartg": { + "name": "Deneyap Kart G", + "variant": VARIANT_ESP32C3, + }, + "deneyapkart": { + "name": "Deneyap Kart", + "variant": VARIANT_ESP32, + }, + "deneyapmini": { + "name": "Deneyap Mini", + "variant": VARIANT_ESP32S2, + }, + "denky32": { + "name": "Denky32 (WROOM32)", + "variant": VARIANT_ESP32, + }, + "denky_d4": { + "name": "Denky D4 (PICO-V3-02)", + "variant": VARIANT_ESP32, + }, + "dfrobot_beetle_esp32c3": { + "name": "DFRobot Beetle ESP32-C3", + "variant": VARIANT_ESP32C3, + }, + "dfrobot_firebeetle2_esp32s3": { + "name": "DFRobot Firebeetle 2 ESP32-S3", + "variant": VARIANT_ESP32S3, + }, + "dpu_esp32": { + "name": "TAMC DPU ESP32", + "variant": VARIANT_ESP32, + }, + "esp320": { + "name": "Electronic SweetPeas ESP320", + "variant": VARIANT_ESP32, + }, + "esp32-c3-devkitm-1": { + "name": "Espressif ESP32-C3-DevKitM-1", + "variant": VARIANT_ESP32C3, + }, + "esp32cam": { + "name": "AI Thinker ESP32-CAM", + "variant": VARIANT_ESP32, + }, + "esp32-devkitlipo": { + "name": "OLIMEX ESP32-DevKit-LiPo", + "variant": VARIANT_ESP32, + }, + "esp32dev": { + "name": "Espressif ESP32 Dev Module", + "variant": VARIANT_ESP32, + }, + "esp32doit-devkit-v1": { + "name": "DOIT ESP32 DEVKIT V1", + "variant": VARIANT_ESP32, + }, + "esp32doit-espduino": { + "name": "DOIT ESPduino32", + "variant": VARIANT_ESP32, + }, + "esp32-evb": { + "name": "OLIMEX ESP32-EVB", + "variant": VARIANT_ESP32, + }, + "esp32-gateway": { + "name": "OLIMEX ESP32-GATEWAY", + "variant": VARIANT_ESP32, + }, + "esp32-poe-iso": { + "name": "OLIMEX ESP32-PoE-ISO", + "variant": VARIANT_ESP32, + }, + "esp32-poe": { + "name": "OLIMEX ESP32-PoE", + "variant": VARIANT_ESP32, + }, + "esp32-pro": { + "name": "OLIMEX ESP32-PRO", + "variant": VARIANT_ESP32, + }, + "esp32-s2-franzininho": { + "name": "Franzininho WiFi Board", + "variant": VARIANT_ESP32S2, + }, + "esp32-s2-kaluga-1": { + "name": "Espressif ESP32-S2-Kaluga-1 Kit", + "variant": VARIANT_ESP32S2, + }, + "esp32-s2-saola-1": { + "name": "Espressif ESP32-S2-Saola-1", + "variant": VARIANT_ESP32S2, + }, + "esp32s3box": { + "name": "Espressif ESP32-S3-Box", + "variant": VARIANT_ESP32S3, + }, + "esp32s3camlcd": { + "name": "ESP32S3 CAM LCD", + "variant": VARIANT_ESP32S3, + }, + "esp32-s3-devkitc-1": { + "name": "Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)", + "variant": VARIANT_ESP32S3, + }, + "esp32thing": { + "name": "SparkFun ESP32 Thing", + "variant": VARIANT_ESP32, + }, + "esp32thing_plus": { + "name": "SparkFun ESP32 Thing Plus", + "variant": VARIANT_ESP32, + }, + "esp32vn-iot-uno": { + "name": "ESP32vn IoT Uno", + "variant": VARIANT_ESP32, + }, + "espea32": { + "name": "April Brother ESPea32", + "variant": VARIANT_ESP32, + }, + "espectro32": { + "name": "ESPectro32", + "variant": VARIANT_ESP32, + }, + "espino32": { + "name": "ESPino32", + "variant": VARIANT_ESP32, + }, + "esp-wrover-kit": { + "name": "Espressif ESP-WROVER-KIT", + "variant": VARIANT_ESP32, + }, + "etboard": { + "name": "ETBoard", + "variant": VARIANT_ESP32, + }, + "featheresp32": { + "name": "Adafruit ESP32 Feather", + "variant": VARIANT_ESP32, + }, + "featheresp32-s2": { + "name": "Adafruit ESP32-S2 Feather Development Board", + "variant": VARIANT_ESP32S2, + }, + "firebeetle32": { + "name": "FireBeetle-ESP32", + "variant": VARIANT_ESP32, + }, + "fm-devkit": { + "name": "ESP32 FM DevKit", + "variant": VARIANT_ESP32, + }, + "franzininho_wifi_esp32s2": { + "name": "Franzininho WiFi", + "variant": VARIANT_ESP32S2, + }, + "franzininho_wifi_msc_esp32s2": { + "name": "Franzininho WiFi MSC", + "variant": VARIANT_ESP32S2, + }, + "frogboard": { + "name": "Frog Board ESP32", + "variant": VARIANT_ESP32, + }, + "healthypi4": { + "name": "ProtoCentral HealthyPi 4", + "variant": VARIANT_ESP32, + }, + "heltec_wifi_kit_32": { + "name": "Heltec WiFi Kit 32", + "variant": VARIANT_ESP32, + }, + "heltec_wifi_kit_32_v2": { + "name": "Heltec WiFi Kit 32 (V2)", + "variant": VARIANT_ESP32, + }, + "heltec_wifi_lora_32": { + "name": "Heltec WiFi LoRa 32", + "variant": VARIANT_ESP32, + }, + "heltec_wifi_lora_32_V2": { + "name": "Heltec WiFi LoRa 32 (V2)", + "variant": VARIANT_ESP32, + }, + "heltec_wireless_stick_lite": { + "name": "Heltec Wireless Stick Lite", + "variant": VARIANT_ESP32, + }, + "heltec_wireless_stick": { + "name": "Heltec Wireless Stick", + "variant": VARIANT_ESP32, + }, + "honeylemon": { + "name": "HONEYLemon", + "variant": VARIANT_ESP32, + }, + "hornbill32dev": { + "name": "Hornbill ESP32 Dev", + "variant": VARIANT_ESP32, + }, + "hornbill32minima": { + "name": "Hornbill ESP32 Minima", + "variant": VARIANT_ESP32, + }, + "imbrios-logsens-v1p1": { + "name": "Imbrios LogSens V1P1", + "variant": VARIANT_ESP32, + }, + "inex_openkb": { + "name": "INEX OpenKB", + "variant": VARIANT_ESP32, + }, + "intorobot": { + "name": "IntoRobot Fig", + "variant": VARIANT_ESP32, + }, + "iotaap_magnolia": { + "name": "IoTaaP Magnolia", + "variant": VARIANT_ESP32, + }, + "iotbusio": { + "name": "oddWires IoT-Bus Io", + "variant": VARIANT_ESP32, + }, + "iotbusproteus": { + "name": "oddWires IoT-Bus Proteus", + "variant": VARIANT_ESP32, + }, + "kb32-ft": { + "name": "MakerAsia KB32-FT", + "variant": VARIANT_ESP32, + }, + "kits-edu": { + "name": "KITS ESP32 EDU", + "variant": VARIANT_ESP32, + }, + "labplus_mpython": { + "name": "Labplus mPython", + "variant": VARIANT_ESP32, + }, + "lionbit": { + "name": "Lion:Bit Dev Board", + "variant": VARIANT_ESP32, + }, + "lolin32_lite": { + "name": "WEMOS LOLIN32 Lite", + "variant": VARIANT_ESP32, + }, + "lolin32": { + "name": "WEMOS LOLIN32", + "variant": VARIANT_ESP32, + }, + "lolin_c3_mini": { + "name": "WEMOS LOLIN C3 Mini", + "variant": VARIANT_ESP32C3, + }, + "lolin_d32": { + "name": "WEMOS LOLIN D32", + "variant": VARIANT_ESP32, + }, + "lolin_d32_pro": { + "name": "WEMOS LOLIN D32 PRO", + "variant": VARIANT_ESP32, + }, + "lolin_s2_mini": { + "name": "WEMOS LOLIN S2 Mini", + "variant": VARIANT_ESP32S2, + }, + "lolin_s2_pico": { + "name": "WEMOS LOLIN S2 PICO", + "variant": VARIANT_ESP32S2, + }, + "lolin_s3": { + "name": "WEMOS LOLIN S3", + "variant": VARIANT_ESP32S3, + }, + "lopy4": { + "name": "Pycom LoPy4", + "variant": VARIANT_ESP32, + }, + "lopy": { + "name": "Pycom LoPy", + "variant": VARIANT_ESP32, + }, + "m5stack-atom": { + "name": "M5Stack-ATOM", + "variant": VARIANT_ESP32, + }, + "m5stack-core2": { + "name": "M5Stack Core2", + "variant": VARIANT_ESP32, + }, + "m5stack-core-esp32": { + "name": "M5Stack Core ESP32", + "variant": VARIANT_ESP32, + }, + "m5stack-coreink": { + "name": "M5Stack-Core Ink", + "variant": VARIANT_ESP32, + }, + "m5stack-fire": { + "name": "M5Stack FIRE", + "variant": VARIANT_ESP32, + }, + "m5stack-grey": { + "name": "M5Stack GREY ESP32", + "variant": VARIANT_ESP32, + }, + "m5stack-station": { + "name": "M5Stack Station", + "variant": VARIANT_ESP32, + }, + "m5stack-timer-cam": { + "name": "M5Stack Timer CAM", + "variant": VARIANT_ESP32, + }, + "m5stick-c": { + "name": "M5Stick-C", + "variant": VARIANT_ESP32, + }, + "magicbit": { + "name": "MagicBit", + "variant": VARIANT_ESP32, + }, + "mgbot-iotik32a": { + "name": "MGBOT IOTIK 32A", + "variant": VARIANT_ESP32, + }, + "mgbot-iotik32b": { + "name": "MGBOT IOTIK 32B", + "variant": VARIANT_ESP32, + }, + "mhetesp32devkit": { + "name": "MH ET LIVE ESP32DevKIT", + "variant": VARIANT_ESP32, + }, + "mhetesp32minikit": { + "name": "MH ET LIVE ESP32MiniKit", + "variant": VARIANT_ESP32, + }, + "microduino-core-esp32": { + "name": "Microduino Core ESP32", + "variant": VARIANT_ESP32, + }, + "micros2": { + "name": "microS2", + "variant": VARIANT_ESP32S2, + }, + "minimain_esp32s2": { + "name": "Deparment of Alchemy MiniMain ESP32-S2", + "variant": VARIANT_ESP32S2, + }, + "nano32": { + "name": "MakerAsia Nano32", + "variant": VARIANT_ESP32, + }, + "nina_w10": { + "name": "u-blox NINA-W10 series", + "variant": VARIANT_ESP32, + }, + "node32s": { + "name": "Node32s", + "variant": VARIANT_ESP32, + }, + "nodemcu-32s2": { + "name": "Ai-Thinker NodeMCU-32S2 (ESP-12K)", + "variant": VARIANT_ESP32S2, + }, + "nodemcu-32s": { + "name": "NodeMCU-32S", + "variant": VARIANT_ESP32, + }, + "nscreen-32": { + "name": "YeaCreate NSCREEN-32", + "variant": VARIANT_ESP32, + }, + "odroid_esp32": { + "name": "ODROID-GO", + "variant": VARIANT_ESP32, + }, + "onehorse32dev": { + "name": "Onehorse ESP32 Dev Module", + "variant": VARIANT_ESP32, + }, + "oroca_edubot": { + "name": "OROCA EduBot", + "variant": VARIANT_ESP32, + }, + "pico32": { + "name": "ESP32 Pico Kit", + "variant": VARIANT_ESP32, + }, + "piranha_esp32": { + "name": "Fishino Piranha ESP-32", + "variant": VARIANT_ESP32, + }, + "pocket_32": { + "name": "Dongsen Tech Pocket 32", + "variant": VARIANT_ESP32, + }, + "pycom_gpy": { + "name": "Pycom GPy", + "variant": VARIANT_ESP32, + }, + "qchip": { + "name": "Qchip", + "variant": VARIANT_ESP32, + }, + "quantum": { + "name": "Noduino Quantum", + "variant": VARIANT_ESP32, + }, + "seeed_xiao_esp32c3": { + "name": "Seeed Studio XIAO ESP32C3", + "variant": VARIANT_ESP32C3, + }, + "sensesiot_weizen": { + "name": "LOGISENSES Senses Weizen", + "variant": VARIANT_ESP32, + }, + "sg-o_airMon": { + "name": "SG-O AirMon", + "variant": VARIANT_ESP32, + }, + "s_odi_ultra": { + "name": "S.ODI Ultra v1", + "variant": VARIANT_ESP32, + }, + "sparkfun_esp32_iot_redboard": { + "name": "SparkFun ESP32 IoT RedBoard", + "variant": VARIANT_ESP32, + }, + "sparkfun_esp32micromod": { + "name": "SparkFun ESP32 MicroMod", + "variant": VARIANT_ESP32, + }, + "sparkfun_esp32s2_thing_plus_c": { + "name": "SparkFun ESP32 Thing Plus C", + "variant": VARIANT_ESP32, + }, + "sparkfun_esp32s2_thing_plus": { + "name": "SparkFun ESP32-S2 Thing Plus", + "variant": VARIANT_ESP32S2, + }, + "sparkfun_lora_gateway_1-channel": { + "name": "SparkFun LoRa Gateway 1-Channel", + "variant": VARIANT_ESP32, + }, + "tamc_termod_s3": { + "name": "TAMC Termod S3", + "variant": VARIANT_ESP32S3, + }, + "tinypico": { + "name": "Unexpected Maker TinyPICO", + "variant": VARIANT_ESP32, + }, + "trueverit-iot-driver-mk2": { + "name": "Trueverit ESP32 Universal IoT Driver MK II", + "variant": VARIANT_ESP32, + }, + "trueverit-iot-driver-mk3": { + "name": "Trueverit ESP32 Universal IoT Driver MK III", + "variant": VARIANT_ESP32, + }, + "trueverit-iot-driver": { + "name": "Trueverit ESP32 Universal IoT Driver", + "variant": VARIANT_ESP32, + }, + "ttgo-lora32-v1": { + "name": "TTGO LoRa32-OLED V1", + "variant": VARIANT_ESP32, + }, + "ttgo-lora32-v21": { + "name": "TTGO LoRa32-OLED v2.1.6", + "variant": VARIANT_ESP32, + }, + "ttgo-lora32-v2": { + "name": "TTGO LoRa32-OLED V2", + "variant": VARIANT_ESP32, + }, + "ttgo-t1": { + "name": "TTGO T1", + "variant": VARIANT_ESP32, + }, + "ttgo-t7-v13-mini32": { + "name": "TTGO T7 V1.3 Mini32", + "variant": VARIANT_ESP32, + }, + "ttgo-t7-v14-mini32": { + "name": "TTGO T7 V1.4 Mini32", + "variant": VARIANT_ESP32, + }, + "ttgo-t-beam": { + "name": "TTGO T-Beam", + "variant": VARIANT_ESP32, + }, + "ttgo-t-oi-plus": { + "name": "TTGO T-OI PLUS RISC-V ESP32-C3", + "variant": VARIANT_ESP32C3, + }, + "ttgo-t-watch": { + "name": "TTGO T-Watch", + "variant": VARIANT_ESP32, + }, + "turta_iot_node": { + "name": "Turta IoT Node", + "variant": VARIANT_ESP32, + }, + "um_feathers2": { + "name": "Unexpected Maker FeatherS2", + "variant": VARIANT_ESP32S2, + }, + "um_feathers2_neo": { + "name": "Unexpected Maker FeatherS2 Neo", + "variant": VARIANT_ESP32S2, + }, + "um_feathers3": { + "name": "Unexpected Maker FeatherS3", + "variant": VARIANT_ESP32S3, + }, + "um_pros3": { + "name": "Unexpected Maker PROS3", + "variant": VARIANT_ESP32S3, + }, + "um_rmp": { + "name": "Unexpected Maker RMP", + "variant": VARIANT_ESP32S2, + }, + "um_tinys2": { + "name": "Unexpected Maker TinyS2", + "variant": VARIANT_ESP32S2, + }, + "um_tinys3": { + "name": "Unexpected Maker TinyS3", + "variant": VARIANT_ESP32S3, + }, + "unphone7": { + "name": "unPhone 7", + "variant": VARIANT_ESP32, + }, + "unphone8": { + "name": "unPhone 8", + "variant": VARIANT_ESP32S3, + }, + "unphone9": { + "name": "unPhone 9", + "variant": VARIANT_ESP32S3, + }, + "upesy_wroom": { + "name": "uPesy ESP32 Wroom DevKit", + "variant": VARIANT_ESP32, + }, + "upesy_wrover": { + "name": "uPesy ESP32 Wrover DevKit", + "variant": VARIANT_ESP32, + }, + "vintlabs-devkit-v1": { + "name": "VintLabs ESP32 Devkit", + "variant": VARIANT_ESP32, + }, + "watchy": { + "name": "SQFMI Watchy v2.0", + "variant": VARIANT_ESP32, + }, + "wemosbat": { + "name": "WeMos WiFi and Bluetooth Battery", + "variant": VARIANT_ESP32, + }, + "wemos_d1_mini32": { + "name": "WEMOS D1 MINI ESP32", + "variant": VARIANT_ESP32, + }, + "wemos_d1_uno32": { + "name": "WEMOS D1 R32", + "variant": VARIANT_ESP32, + }, + "wesp32": { + "name": "Silicognition wESP32", + "variant": VARIANT_ESP32, + }, + "widora-air": { + "name": "Widora AIR", + "variant": VARIANT_ESP32, + }, + "wifiduino32c3": { + "name": "Blinker WiFiduinoV2 (ESP32-C3)", + "variant": VARIANT_ESP32C3, + }, + "wifiduino32": { + "name": "Blinker WiFiduino32", + "variant": VARIANT_ESP32, + }, + "wifiduino32s3": { + "name": "Blinker WiFiduino32S3", + "variant": VARIANT_ESP32S3, + }, + "wipy3": { + "name": "Pycom WiPy3", + "variant": VARIANT_ESP32, + }, + "wt32-eth01": { + "name": "Wireless-Tag WT32-ETH01 Ethernet Module", + "variant": VARIANT_ESP32, + }, + "xinabox_cw02": { + "name": "XinaBox CW02", + "variant": VARIANT_ESP32, + }, } diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py index d92b449ee9..d13df01d3a 100644 --- a/esphome/components/esp32/const.py +++ b/esphome/components/esp32/const.py @@ -4,6 +4,11 @@ KEY_ESP32 = "esp32" KEY_BOARD = "board" KEY_VARIANT = "variant" KEY_SDKCONFIG_OPTIONS = "sdkconfig_options" +KEY_COMPONENTS = "components" +KEY_REPO = "repo" +KEY_REF = "ref" +KEY_REFRESH = "refresh" +KEY_PATH = "path" VARIANT_ESP32 = "ESP32" VARIANT_ESP32S2 = "ESP32S2" diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 6123d83a34..512a8857b6 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #if ESP_IDF_VERSION_MAJOR >= 4 @@ -23,7 +24,7 @@ void loop(); namespace esphome { void IRAM_ATTR HOT yield() { vPortYield(); } -uint32_t IRAM_ATTR HOT millis() { return (uint32_t)(esp_timer_get_time() / 1000ULL); } +uint32_t IRAM_ATTR HOT millis() { return (uint32_t) (esp_timer_get_time() / 1000ULL); } void IRAM_ATTR HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/components/esp32/gpio_idf.cpp b/esphome/components/esp32/gpio.cpp similarity index 76% rename from esphome/components/esp32/gpio_idf.cpp rename to esphome/components/esp32/gpio.cpp index 498843ebff..7896597d3e 100644 --- a/esphome/components/esp32/gpio_idf.cpp +++ b/esphome/components/esp32/gpio.cpp @@ -1,20 +1,19 @@ -#ifdef USE_ESP32_FRAMEWORK_ESP_IDF +#ifdef USE_ESP32 -#include "gpio_idf.h" +#include "gpio.h" #include "esphome/core/log.h" +#include namespace esphome { namespace esp32 { static const char *const TAG = "esp32"; -bool IDFInternalGPIOPin::isr_service_installed = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +bool ESP32InternalGPIOPin::isr_service_installed = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static gpio_mode_t IRAM_ATTR flags_to_mode(gpio::Flags flags) { flags = (gpio::Flags)(flags & ~(gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN)); - if (flags == gpio::FLAG_NONE) { - return GPIO_MODE_DISABLE; - } else if (flags == gpio::FLAG_INPUT) { + if (flags == gpio::FLAG_INPUT) { return GPIO_MODE_INPUT; } else if (flags == gpio::FLAG_OUTPUT) { return GPIO_MODE_OUTPUT; @@ -25,7 +24,7 @@ static gpio_mode_t IRAM_ATTR flags_to_mode(gpio::Flags flags) { } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT)) { return GPIO_MODE_INPUT_OUTPUT; } else { - // unsupported + // unsupported or gpio::FLAG_NONE return GPIO_MODE_DISABLE; } } @@ -35,14 +34,14 @@ struct ISRPinArg { bool inverted; }; -ISRInternalGPIOPin IDFInternalGPIOPin::to_isr() const { +ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const { auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) arg->pin = pin_; arg->inverted = inverted_; return ISRInternalGPIOPin((void *) arg); } -void IDFInternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { +void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { gpio_int_type_t idf_type = GPIO_INTR_ANYEDGE; switch (type) { case gpio::INTERRUPT_RISING_EDGE: @@ -74,13 +73,13 @@ void IDFInternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio: gpio_isr_handler_add(pin_, func, arg); } -std::string IDFInternalGPIOPin::dump_summary() const { +std::string ESP32InternalGPIOPin::dump_summary() const { char buffer[32]; - snprintf(buffer, sizeof(buffer), "GPIO%u", static_cast(pin_)); + snprintf(buffer, sizeof(buffer), "GPIO%" PRIu32, static_cast(pin_)); return buffer; } -void IDFInternalGPIOPin::setup() { +void ESP32InternalGPIOPin::setup() { gpio_config_t conf{}; conf.pin_bit_mask = 1ULL << static_cast(pin_); conf.mode = flags_to_mode(flags_); @@ -88,14 +87,16 @@ void IDFInternalGPIOPin::setup() { conf.pull_down_en = flags_ & gpio::FLAG_PULLDOWN ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE; conf.intr_type = GPIO_INTR_DISABLE; gpio_config(&conf); - gpio_set_drive_capability(pin_, drive_strength_); + if (flags_ & gpio::FLAG_OUTPUT) { + gpio_set_drive_capability(pin_, drive_strength_); + } } -void IDFInternalGPIOPin::pin_mode(gpio::Flags flags) { +void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) { // can't call gpio_config here because that logs in esp-idf which may cause issues gpio_set_direction(pin_, flags_to_mode(flags)); gpio_pull_mode_t pull_mode = GPIO_FLOATING; - if (flags & (gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN)) { + if ((flags & gpio::FLAG_PULLUP) && (flags & gpio::FLAG_PULLDOWN)) { pull_mode = GPIO_PULLUP_PULLDOWN; } else if (flags & gpio::FLAG_PULLUP) { pull_mode = GPIO_PULLUP_ONLY; @@ -105,9 +106,9 @@ void IDFInternalGPIOPin::pin_mode(gpio::Flags flags) { gpio_set_pull_mode(pin_, pull_mode); } -bool IDFInternalGPIOPin::digital_read() { return bool(gpio_get_level(pin_)) != inverted_; } -void IDFInternalGPIOPin::digital_write(bool value) { gpio_set_level(pin_, value != inverted_ ? 1 : 0); } -void IDFInternalGPIOPin::detach_interrupt() const { gpio_intr_disable(pin_); } +bool ESP32InternalGPIOPin::digital_read() { return bool(gpio_get_level(pin_)) != inverted_; } +void ESP32InternalGPIOPin::digital_write(bool value) { gpio_set_level(pin_, value != inverted_ ? 1 : 0); } +void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(pin_); } } // namespace esp32 @@ -128,7 +129,7 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { auto *arg = reinterpret_cast(arg_); gpio_set_direction(arg->pin, flags_to_mode(flags)); gpio_pull_mode_t pull_mode = GPIO_FLOATING; - if (flags & (gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN)) { + if ((flags & gpio::FLAG_PULLUP) && (flags & gpio::FLAG_PULLDOWN)) { pull_mode = GPIO_PULLUP_PULLDOWN; } else if (flags & gpio::FLAG_PULLUP) { pull_mode = GPIO_PULLUP_ONLY; @@ -140,4 +141,4 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { } // namespace esphome -#endif // USE_ESP32_FRAMEWORK_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/esp32/gpio_idf.h b/esphome/components/esp32/gpio.h similarity index 90% rename from esphome/components/esp32/gpio_idf.h rename to esphome/components/esp32/gpio.h index a07d11378a..23b723e0b4 100644 --- a/esphome/components/esp32/gpio_idf.h +++ b/esphome/components/esp32/gpio.h @@ -1,13 +1,13 @@ #pragma once -#ifdef USE_ESP32_FRAMEWORK_ESP_IDF +#ifdef USE_ESP32 #include "esphome/core/hal.h" #include namespace esphome { namespace esp32 { -class IDFInternalGPIOPin : public InternalGPIOPin { +class ESP32InternalGPIOPin : public InternalGPIOPin { public: void set_pin(gpio_num_t pin) { pin_ = pin; } void set_inverted(bool inverted) { inverted_ = inverted; } @@ -37,4 +37,4 @@ class IDFInternalGPIOPin : public InternalGPIOPin { } // namespace esp32 } // namespace esphome -#endif // USE_ESP32_FRAMEWORK_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index 5819943f37..7848d1d552 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -38,8 +38,7 @@ from .gpio_esp32_s3 import esp32_s3_validate_gpio_pin, esp32_s3_validate_support from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports -IDFInternalGPIOPin = esp32_ns.class_("IDFInternalGPIOPin", cg.InternalGPIOPin) -ArduinoInternalGPIOPin = esp32_ns.class_("ArduinoInternalGPIOPin", cg.InternalGPIOPin) +ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin) def _lookup_pin(value): @@ -105,23 +104,30 @@ _esp32_validations = { def validate_gpio_pin(value): value = _translate_pin(value) + board = CORE.data[KEY_ESP32][KEY_BOARD] + board_pins = boards.ESP32_BOARD_PINS.get(board, {}) + + # Resolved aliased board pins (shorthand when two boards have the same pin configuration) + while isinstance(board_pins, str): + board_pins = boards.ESP32_BOARD_PINS[board_pins] + + if value in board_pins.values(): + return value + variant = CORE.data[KEY_ESP32][KEY_VARIANT] if variant not in _esp32_validations: - raise cv.Invalid("Unsupported ESP32 variant {variant}") + raise cv.Invalid(f"Unsupported ESP32 variant {variant}") return _esp32_validations[variant].pin_validation(value) def validate_supports(value): mode = value[CONF_MODE] - is_input = mode[CONF_INPUT] is_output = mode[CONF_OUTPUT] is_open_drain = mode[CONF_OPEN_DRAIN] - is_pullup = mode[CONF_PULLUP] - is_pulldown = mode[CONF_PULLDOWN] variant = CORE.data[KEY_ESP32][KEY_VARIANT] if variant not in _esp32_validations: - raise cv.Invalid("Unsupported ESP32 variant {variant}") + raise cv.Invalid(f"Unsupported ESP32 variant {variant}") if is_open_drain and not is_output: raise cv.Invalid( @@ -129,26 +135,6 @@ def validate_supports(value): ) value = _esp32_validations[variant].usage_validation(value) - if CORE.using_arduino: - # (input, output, open_drain, pullup, pulldown) - supported_modes = { - # INPUT - (True, False, False, False, False), - # OUTPUT - (False, True, False, False, False), - # INPUT_PULLUP - (True, False, False, True, False), - # INPUT_PULLDOWN - (True, False, False, False, True), - # OUTPUT_OPEN_DRAIN - (False, True, True, False, False), - } - key = (is_input, is_output, is_open_drain, is_pullup, is_pulldown) - if key not in supported_modes: - raise cv.Invalid( - "This pin mode is not supported on ESP32 for arduino frameworks", - [CONF_MODE], - ) return value @@ -163,18 +149,10 @@ DRIVE_STRENGTHS = { gpio_num_t = cg.global_ns.enum("gpio_num_t") -def _choose_pin_declaration(value): - if CORE.using_esp_idf: - return cv.declare_id(IDFInternalGPIOPin)(value) - if CORE.using_arduino: - return cv.declare_id(ArduinoInternalGPIOPin)(value) - raise NotImplementedError - - CONF_DRIVE_STRENGTH = "drive_strength" ESP32_PIN_SCHEMA = cv.All( { - cv.GenerateID(): _choose_pin_declaration, + cv.GenerateID(): cv.declare_id(ESP32InternalGPIOPin), cv.Required(CONF_NUMBER): validate_gpio_pin, cv.Optional(CONF_MODE, default={}): cv.Schema( { @@ -186,8 +164,7 @@ ESP32_PIN_SCHEMA = cv.All( } ), cv.Optional(CONF_INVERTED, default=False): cv.boolean, - cv.SplitDefault(CONF_DRIVE_STRENGTH, esp32_idf="20mA"): cv.All( - cv.only_with_esp_idf, + cv.Optional(CONF_DRIVE_STRENGTH, default="20mA"): cv.All( cv.float_with_unit("current", "mA", optional_unit=True), cv.enum(DRIVE_STRENGTHS), ), @@ -200,10 +177,7 @@ ESP32_PIN_SCHEMA = cv.All( async def esp32_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] - if CORE.using_esp_idf: - cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}"))) - else: - cg.add(var.set_pin(num)) + cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}"))) cg.add(var.set_inverted(config[CONF_INVERTED])) if CONF_DRIVE_STRENGTH in config: cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH])) diff --git a/esphome/components/esp32/gpio_esp32.py b/esphome/components/esp32/gpio_esp32.py index dbafb73dba..66ba2ffa62 100644 --- a/esphome/components/esp32/gpio_esp32.py +++ b/esphome/components/esp32/gpio_esp32.py @@ -42,7 +42,7 @@ def esp32_validate_gpio_pin(value): "See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins", value, ) - if value in (20, 24, 28, 29, 30, 31): + if value in (24, 28, 29, 30, 31): # These pins are not exposed in GPIO mux (reason unknown) # but they're missing from IO_MUX list in datasheet raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32s.") diff --git a/esphome/components/esp32/post_build.py.script b/esphome/components/esp32/post_build.py.script index 7feaf9e8e5..c941bdb386 100644 --- a/esphome/components/esp32/post_build.py.script +++ b/esphome/components/esp32/post_build.py.script @@ -1,13 +1,30 @@ # Source https://github.com/letscontrolit/ESPEasy/pull/3845#issuecomment-1005864664 -import esptool - # pylint: disable=E0602 Import("env") # noqa +import os +import shutil + +if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: + try: + import esptool + except ImportError: + env.Execute("$PYTHONEXE -m pip install esptool") +else: + import subprocess +from SCons.Script import ARGUMENTS + +# Copy over the default sdkconfig. +from os import path +if path.exists("./sdkconfig.defaults"): + os.makedirs(".temp", exist_ok=True) + shutil.copy("./sdkconfig.defaults", "./.temp/sdkconfig-esp32-idf") def esp32_create_combined_bin(source, target, env): - print("Generating combined binary for serial flashing") + verbose = bool(int(ARGUMENTS.get("PIOVERBOSE", "0"))) + if verbose: + print("Generating combined binary for serial flashing") app_offset = 0x10000 new_file_name = env.subst("$BUILD_DIR/${PROGNAME}-factory.bin") @@ -24,20 +41,26 @@ def esp32_create_combined_bin(source, target, env): "--flash_size", flash_size, ] - print(" Offset | File") + if verbose: + print(" Offset | File") for section in sections: sect_adr, sect_file = section.split(" ", 1) - print(f" - {sect_adr} | {sect_file}") + if verbose: + print(f" - {sect_adr} | {sect_file}") cmd += [sect_adr, sect_file] - print(f" - {hex(app_offset)} | {firmware_name}") cmd += [hex(app_offset), firmware_name] - print() - print(f"Using esptool.py arguments: {' '.join(cmd)}") - print() - esptool.main(cmd) + if verbose: + print(f" - {hex(app_offset)} | {firmware_name}") + print() + print(f"Using esptool.py arguments: {' '.join(cmd)}") + print() + if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: + esptool.main(cmd) + else: + subprocess.run(["esptool.py", *cmd]) # pylint: disable=E0602 env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) # noqa diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 8c2b67a942..f90b8a4603 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -5,6 +5,7 @@ #include "esphome/core/log.h" #include #include +#include #include #include @@ -36,6 +37,7 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { save.key = key; save.data.assign(data, data + len); s_pending_save.emplace_back(save); + ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %d", key.c_str(), len); return true; } bool load(uint8_t *data, size_t len) override { @@ -65,6 +67,8 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { if (err != 0) { ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key.c_str(), esp_err_to_name(err)); return false; + } else { + ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %d", key.c_str(), len); } return true; } @@ -73,7 +77,6 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { class ESP32Preferences : public ESPPreferences { public: uint32_t nvs_handle; - uint32_t current_offset = 0; void open() { nvs_flash_init(); @@ -97,12 +100,9 @@ class ESP32Preferences : public ESPPreferences { ESPPreferenceObject make_preference(size_t length, uint32_t type) override { auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) pref->nvs_handle = nvs_handle; - current_offset += length; - uint32_t keyval = current_offset ^ type; - char keybuf[16]; - snprintf(keybuf, sizeof(keybuf), "%d", keyval); - pref->key = keybuf; // copied to std::string + uint32_t keyval = type; + pref->key = str_sprintf("%" PRIu32, keyval); return ESPPreferenceObject(pref); } @@ -111,22 +111,40 @@ class ESP32Preferences : public ESPPreferences { if (s_pending_save.empty()) return true; - ESP_LOGD(TAG, "Saving preferences to flash..."); + ESP_LOGD(TAG, "Saving %d preferences to flash...", s_pending_save.size()); // goal try write all pending saves even if one fails - bool any_failed = false; + int cached = 0, written = 0, failed = 0; + esp_err_t last_err = ESP_OK; + std::string last_key{}; // go through vector from back to front (makes erase easier/more efficient) for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) { const auto &save = s_pending_save[i]; - esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size()); - if (err != 0) { - ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(), - esp_err_to_name(err)); - any_failed = true; - continue; + ESP_LOGVV(TAG, "Checking if NVS data %s has changed", save.key.c_str()); + if (is_changed(nvs_handle, save)) { + esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size()); + ESP_LOGV(TAG, "sync: key: %s, len: %d", save.key.c_str(), save.data.size()); + if (err != 0) { + ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(), + esp_err_to_name(err)); + failed++; + last_err = err; + last_key = save.key; + continue; + } + written++; + } else { + ESP_LOGV(TAG, "NVS data not changed skipping %s len=%u", save.key.c_str(), save.data.size()); + cached++; } s_pending_save.erase(s_pending_save.begin() + i); } + ESP_LOGD(TAG, "Saving %d preferences to flash: %d cached, %d written, %d failed", cached + written + failed, cached, + written, failed); + if (failed > 0) { + ESP_LOGE(TAG, "Error saving %d preferences to flash. Last error=%s for key=%s", failed, esp_err_to_name(last_err), + last_key.c_str()); + } // note: commit on esp-idf currently is a no-op, nvs_set_blob always writes esp_err_t err = nvs_commit(nvs_handle); @@ -135,7 +153,34 @@ class ESP32Preferences : public ESPPreferences { return false; } - return !any_failed; + return failed == 0; + } + bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) { + NVSData stored_data{}; + size_t actual_len; + esp_err_t err = nvs_get_blob(nvs_handle, to_save.key.c_str(), nullptr, &actual_len); + if (err != 0) { + ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", to_save.key.c_str(), esp_err_to_name(err)); + return true; + } + stored_data.data.resize(actual_len); + err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.data.data(), &actual_len); + if (err != 0) { + ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err)); + return true; + } + return to_save.data != stored_data.data; + } + + bool reset() override { + ESP_LOGD(TAG, "Cleaning up preferences in flash..."); + s_pending_save.clear(); + + nvs_flash_deinit(); + nvs_flash_erase(); + // Make the handle invalid to prevent any saves until restart + nvs_handle = 0; + return true; } }; diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 4b5c741ad9..f508cecb87 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -2,26 +2,56 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.core import CORE -from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant, const DEPENDENCIES = ["esp32"] CODEOWNERS = ["@jesserockz"] -CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"] +CONFLICTS_WITH = ["esp32_ble_beacon"] + +CONF_BLE_ID = "ble_id" +CONF_IO_CAPABILITY = "io_capability" + +NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble") ESP32BLE = esp32_ble_ns.class_("ESP32BLE", cg.Component) +GAPEventHandler = esp32_ble_ns.class_("GAPEventHandler") +GATTcEventHandler = esp32_ble_ns.class_("GATTcEventHandler") +GATTsEventHandler = esp32_ble_ns.class_("GATTsEventHandler") + +IoCapability = esp32_ble_ns.enum("IoCapability") +IO_CAPABILITY = { + "none": IoCapability.IO_CAP_NONE, + "keyboard_only": IoCapability.IO_CAP_IN, + "keyboard_display": IoCapability.IO_CAP_KBDISP, + "display_only": IoCapability.IO_CAP_OUT, + "display_yes_no": IoCapability.IO_CAP_IO, +} CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32BLE), + cv.Optional(CONF_IO_CAPABILITY, default="none"): cv.enum( + IO_CAPABILITY, lower=True + ), } ).extend(cv.COMPONENT_SCHEMA) +def validate_variant(_): + variant = get_esp32_variant() + if variant in NO_BLUETOOTH_VARIANTS: + raise cv.Invalid(f"{variant} does not support Bluetooth") + + +FINAL_VALIDATE_SCHEMA = validate_variant + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY])) if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index ecd591d169..6c9124447a 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -4,13 +4,14 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" -#include -#include -#include #include -#include -#include +#include +#include #include +#include +#include +#include +#include #ifdef USE_ARDUINO #include @@ -31,24 +32,17 @@ void ESP32BLE::setup() { return; } +#ifdef USE_ESP32_BLE_SERVER this->advertising_ = new BLEAdvertising(); // NOLINT(cppcoreguidelines-owning-memory) this->advertising_->set_scan_response(true); this->advertising_->set_min_preferred_interval(0x06); this->advertising_->start(); +#endif // USE_ESP32_BLE_SERVER ESP_LOGD(TAG, "BLE setup complete"); } -void ESP32BLE::mark_failed() { - Component::mark_failed(); -#ifdef USE_ESP32_BLE_SERVER - if (this->server_ != nullptr) { - this->server_->mark_failed(); - } -#endif -} - bool ESP32BLE::ble_setup_() { esp_err_t err = nvs_flash_init(); if (err != ESP_OK) { @@ -100,13 +94,16 @@ bool ESP32BLE::ble_setup_() { ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err); return false; } - err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); - return false; + + if (!this->gap_event_handlers_.empty()) { + err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); + return false; + } } - if (this->has_server()) { + if (!this->gatts_event_handlers_.empty()) { err = esp_ble_gatts_register_callback(ESP32BLE::gatts_event_handler); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_register_callback failed: %d", err); @@ -114,7 +111,7 @@ bool ESP32BLE::ble_setup_() { } } - if (this->has_client()) { + if (!this->gattc_event_handlers_.empty()) { err = esp_ble_gattc_register_callback(ESP32BLE::gattc_event_handler); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err); @@ -137,8 +134,7 @@ bool ESP32BLE::ble_setup_() { return false; } - esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE; - err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t)); + err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &(this->io_cap_), sizeof(uint8_t)); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gap_set_security_param failed: %d", err); return false; @@ -158,6 +154,10 @@ void ESP32BLE::loop() { this->real_gatts_event_handler_(ble_event->event_.gatts.gatts_event, ble_event->event_.gatts.gatts_if, &ble_event->event_.gatts.gatts_param); break; + case BLEEvent::GATTC: + this->real_gattc_event_handler_(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if, + &ble_event->event_.gattc.gattc_param); + break; case BLEEvent::GAP: this->real_gap_event_handler_(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param); break; @@ -176,9 +176,8 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { ESP_LOGV(TAG, "(BLE) gap_event_handler - %d", event); - switch (event) { - default: - break; + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler(event, param); } } @@ -191,19 +190,70 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { ESP_LOGV(TAG, "(BLE) gatts_event [esp_gatt_if: %d] - %d", gatts_if, event); -#ifdef USE_ESP32_BLE_SERVER - this->server_->gatts_event_handler(event, gatts_if, param); -#endif + for (auto *gatts_handler : this->gatts_event_handlers_) { + gatts_handler->gatts_event_handler(event, gatts_if, param); + } } +void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + BLEEvent *new_event = new BLEEvent(event, gattc_if, param); // NOLINT(cppcoreguidelines-owning-memory) + global_ble->ble_events_.push(new_event); +} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) + void ESP32BLE::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { - // this->client_->gattc_event_handler(event, gattc_if, param); + ESP_LOGV(TAG, "(BLE) gattc_event [esp_gatt_if: %d] - %d", gattc_if, event); + for (auto *gattc_handler : this->gattc_event_handlers_) { + gattc_handler->gattc_event_handler(event, gattc_if, param); + } } float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; } -void ESP32BLE::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE:"); } +void ESP32BLE::dump_config() { + const uint8_t *mac_address = esp_bt_dev_get_address(); + if (mac_address) { + const char *io_capability_s; + switch (this->io_cap_) { + case ESP_IO_CAP_OUT: + io_capability_s = "display_only"; + break; + case ESP_IO_CAP_IO: + io_capability_s = "display_yes_no"; + break; + case ESP_IO_CAP_IN: + io_capability_s = "keyboard_only"; + break; + case ESP_IO_CAP_NONE: + io_capability_s = "none"; + break; + case ESP_IO_CAP_KBDISP: + io_capability_s = "keyboard_display"; + break; + default: + io_capability_s = "invalid"; + break; + } + ESP_LOGCONFIG(TAG, "ESP32 BLE:"); + ESP_LOGCONFIG(TAG, " MAC address: %02X:%02X:%02X:%02X:%02X:%02X", mac_address[0], mac_address[1], mac_address[2], + mac_address[3], mac_address[4], mac_address[5]); + ESP_LOGCONFIG(TAG, " IO Capability: %s", io_capability_s); + } else { + ESP_LOGCONFIG(TAG, "ESP32 BLE: bluetooth stack is not enabled"); + } +} + +uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { + uint64_t u = 0; + u |= uint64_t(address[0] & 0xFF) << 40; + u |= uint64_t(address[1] & 0xFF) << 32; + u |= uint64_t(address[2] & 0xFF) << 24; + u |= uint64_t(address[3] & 0xFF) << 16; + u |= uint64_t(address[4] & 0xFF) << 8; + u |= uint64_t(address[5] & 0xFF) << 0; + return u; +} ESP32BLE *global_ble = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 0477dee070..cde17da425 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -5,20 +5,21 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" -#include "queue.h" -#ifdef USE_ESP32_BLE_SERVER -#include "esphome/components/esp32_ble_server/ble_server.h" -#endif +#include "queue.h" +#include "ble_event.h" #ifdef USE_ESP32 #include #include #include + namespace esphome { namespace esp32_ble { +uint64_t ble_addr_to_uint64(const esp_bd_addr_t address); + // NOLINTNEXTLINE(modernize-use-using) typedef struct { void *peer_device; @@ -26,28 +27,46 @@ typedef struct { uint16_t mtu; } conn_status_t; +enum IoCapability { + IO_CAP_OUT = ESP_IO_CAP_OUT, + IO_CAP_IO = ESP_IO_CAP_IO, + IO_CAP_IN = ESP_IO_CAP_IN, + IO_CAP_NONE = ESP_IO_CAP_NONE, + IO_CAP_KBDISP = ESP_IO_CAP_KBDISP, +}; + +class GAPEventHandler { + public: + virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; +}; + +class GATTcEventHandler { + public: + virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) = 0; +}; + +class GATTsEventHandler { + public: + virtual void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t *param) = 0; +}; + class ESP32BLE : public Component { public: + void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; } + void setup() override; void loop() override; void dump_config() override; float get_setup_priority() const override; - void mark_failed() override; - - bool has_server() { -#ifdef USE_ESP32_BLE_SERVER - return this->server_ != nullptr; -#else - return false; -#endif - } - bool has_client() { return false; } BLEAdvertising *get_advertising() { return this->advertising_; } -#ifdef USE_ESP32_BLE_SERVER - void set_server(esp32_ble_server::BLEServer *server) { this->server_ = server; } -#endif + void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); } + void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); } + void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); } + protected: static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); @@ -59,11 +78,13 @@ class ESP32BLE : public Component { bool ble_setup_(); -#ifdef USE_ESP32_BLE_SERVER - esp32_ble_server::BLEServer *server_{nullptr}; -#endif + std::vector gap_event_handlers_; + std::vector gattc_event_handlers_; + std::vector gatts_event_handlers_; + Queue ble_events_; BLEAdvertising *advertising_; + esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp index 31b1f4c383..2083bf5f08 100644 --- a/esphome/components/esp32_ble/ble_advertising.cpp +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -42,6 +42,11 @@ void BLEAdvertising::remove_service_uuid(ESPBTUUID uuid) { this->advertising_uuids_.end()); } +void BLEAdvertising::set_manufacturer_data(uint8_t *data, uint16_t size) { + this->advertising_data_.p_manufacturer_data = data; + this->advertising_data_.manufacturer_len = size; +} + void BLEAdvertising::start() { int num_services = this->advertising_uuids_.size(); if (num_services == 0) { diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h index 01e2ba1295..079bd6c14c 100644 --- a/esphome/components/esp32_ble/ble_advertising.h +++ b/esphome/components/esp32_ble/ble_advertising.h @@ -20,6 +20,7 @@ class BLEAdvertising { void remove_service_uuid(ESPBTUUID uuid); void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; } void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; } + void set_manufacturer_data(uint8_t *data, uint16_t size); void start(); void stop(); diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h new file mode 100644 index 0000000000..1cf63b2fab --- /dev/null +++ b/esphome/components/esp32_ble/ble_event.h @@ -0,0 +1,94 @@ +#pragma once + +#ifdef USE_ESP32 + +#include + +#include +#include +#include + +namespace esphome { +namespace esp32_ble { +// Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop(). +// This class stores each event in a single type. +class BLEEvent { + public: + BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { + this->event_.gap.gap_event = e; + memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t)); + this->type_ = GAP; + }; + + BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { + this->event_.gattc.gattc_event = e; + this->event_.gattc.gattc_if = i; + memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t)); + // Need to also make a copy of relevant event data. + switch (e) { + case ESP_GATTC_NOTIFY_EVT: + this->data.assign(p->notify.value, p->notify.value + p->notify.value_len); + this->event_.gattc.gattc_param.notify.value = this->data.data(); + break; + case ESP_GATTC_READ_CHAR_EVT: + case ESP_GATTC_READ_DESCR_EVT: + this->data.assign(p->read.value, p->read.value + p->read.value_len); + this->event_.gattc.gattc_param.read.value = this->data.data(); + break; + default: + break; + } + this->type_ = GATTC; + }; + + BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { + this->event_.gatts.gatts_event = e; + this->event_.gatts.gatts_if = i; + memcpy(&this->event_.gatts.gatts_param, p, sizeof(esp_ble_gatts_cb_param_t)); + // Need to also make a copy of relevant event data. + switch (e) { + case ESP_GATTS_WRITE_EVT: + this->data.assign(p->write.value, p->write.value + p->write.len); + this->event_.gatts.gatts_param.write.value = this->data.data(); + break; + default: + break; + } + this->type_ = GATTS; + }; + + union { + // NOLINTNEXTLINE(readability-identifier-naming) + struct gap_event { + esp_gap_ble_cb_event_t gap_event; + esp_ble_gap_cb_param_t gap_param; + } gap; + + // NOLINTNEXTLINE(readability-identifier-naming) + struct gattc_event { + esp_gattc_cb_event_t gattc_event; + esp_gatt_if_t gattc_if; + esp_ble_gattc_cb_param_t gattc_param; + } gattc; + + // NOLINTNEXTLINE(readability-identifier-naming) + struct gatts_event { + esp_gatts_cb_event_t gatts_event; + esp_gatt_if_t gatts_if; + esp_ble_gatts_cb_param_t gatts_param; + } gatts; + } event_; + + std::vector data{}; + // NOLINTNEXTLINE(readability-identifier-naming) + enum ble_event_t : uint8_t { + GAP, + GATTC, + GATTS, + } type_; +}; + +} // namespace esp32_ble +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp index 8556aa87df..a50d3dbd42 100644 --- a/esphome/components/esp32_ble/ble_uuid.cpp +++ b/esphome/components/esp32_ble/ble_uuid.cpp @@ -27,8 +27,7 @@ ESPBTUUID ESPBTUUID::from_uint32(uint32_t uuid) { ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) { ESPBTUUID ret; ret.uuid_.len = ESP_UUID_LEN_128; - for (size_t i = 0; i < ESP_UUID_LEN_128; i++) - ret.uuid_.uuid.uuid128[i] = data[i]; + memcpy(ret.uuid_.uuid.uuid128, data, ESP_UUID_LEN_128); return ret; } ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { @@ -91,10 +90,13 @@ ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) { ESPBTUUID ret; ret.uuid_.len = uuid.len; - ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16; - ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32; - for (size_t i = 0; i < ESP_UUID_LEN_128; i++) - ret.uuid_.uuid.uuid128[i] = uuid.uuid.uuid128[i]; + if (uuid.len == ESP_UUID_LEN_16) { + ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16; + } else if (uuid.len == ESP_UUID_LEN_32) { + ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32; + } else if (uuid.len == ESP_UUID_LEN_128) { + memcpy(ret.uuid_.uuid.uuid128, uuid.uuid.uuid128, ESP_UUID_LEN_128); + } return ret; } ESPBTUUID ESPBTUUID::as_128bit() const { @@ -158,30 +160,26 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const { } return false; } -esp_bt_uuid_t ESPBTUUID::get_uuid() { return this->uuid_; } -std::string ESPBTUUID::to_string() { - char sbuf[64]; +esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; } +std::string ESPBTUUID::to_string() const { switch (this->uuid_.len) { case ESP_UUID_LEN_16: - sprintf(sbuf, "0x%02X%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); - break; + return str_snprintf("0x%02X%02X", 6, this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); case ESP_UUID_LEN_32: - sprintf(sbuf, "0x%02X%02X%02X%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff), - (this->uuid_.uuid.uuid32 >> 8 & 0xff), this->uuid_.uuid.uuid32 & 0xff); - break; + return str_snprintf("0x%02X%02X%02X%02X", 10, this->uuid_.uuid.uuid32 >> 24, + (this->uuid_.uuid.uuid32 >> 16 & 0xff), (this->uuid_.uuid.uuid32 >> 8 & 0xff), + this->uuid_.uuid.uuid32 & 0xff); default: case ESP_UUID_LEN_128: - char *bpos = sbuf; + std::string buf; for (int8_t i = 15; i >= 0; i--) { - sprintf(bpos, "%02X", this->uuid_.uuid.uuid128[i]); - bpos += 2; + buf += str_snprintf("%02X", 2, this->uuid_.uuid.uuid128[i]); if (i == 6 || i == 8 || i == 10 || i == 12) - sprintf(bpos++, "-"); + buf += "-"; } - sbuf[47] = '\0'; - break; + return buf; } - return sbuf; + return ""; } } // namespace esp32_ble diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h index f953f9fede..790a57c59d 100644 --- a/esphome/components/esp32_ble/ble_uuid.h +++ b/esphome/components/esp32_ble/ble_uuid.h @@ -32,9 +32,9 @@ class ESPBTUUID { bool operator==(const ESPBTUUID &uuid) const; bool operator!=(const ESPBTUUID &uuid) const { return !(*this == uuid); } - esp_bt_uuid_t get_uuid(); + esp_bt_uuid_t get_uuid() const; - std::string to_string(); + std::string to_string() const; protected: esp_bt_uuid_t uuid_; diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h index 8d05eca058..5b31b97ae2 100644 --- a/esphome/components/esp32_ble/queue.h +++ b/esphome/components/esp32_ble/queue.h @@ -2,16 +2,9 @@ #ifdef USE_ESP32 -#include "esphome/core/component.h" -#include "esphome/core/helpers.h" - -#include #include -#include +#include -#include -#include -#include #include #include @@ -57,84 +50,6 @@ template class Queue { SemaphoreHandle_t m_; }; -// Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop(). -// This class stores each event in a single type. -class BLEEvent { - public: - BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { - this->event_.gap.gap_event = e; - memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t)); - this->type_ = GAP; - }; - - BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { - this->event_.gattc.gattc_event = e; - this->event_.gattc.gattc_if = i; - memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t)); - // Need to also make a copy of notify event data. - switch (e) { - case ESP_GATTC_NOTIFY_EVT: - memcpy(this->event_.gattc.data, p->notify.value, p->notify.value_len); - this->event_.gattc.gattc_param.notify.value = this->event_.gattc.data; - break; - case ESP_GATTC_READ_CHAR_EVT: - case ESP_GATTC_READ_DESCR_EVT: - memcpy(this->event_.gattc.data, p->read.value, p->read.value_len); - this->event_.gattc.gattc_param.read.value = this->event_.gattc.data; - break; - default: - break; - } - this->type_ = GATTC; - }; - - BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { - this->event_.gatts.gatts_event = e; - this->event_.gatts.gatts_if = i; - memcpy(&this->event_.gatts.gatts_param, p, sizeof(esp_ble_gatts_cb_param_t)); - // Need to also make a copy of write data. - switch (e) { - case ESP_GATTS_WRITE_EVT: - memcpy(this->event_.gatts.data, p->write.value, p->write.len); - this->event_.gatts.gatts_param.write.value = this->event_.gatts.data; - break; - default: - break; - } - this->type_ = GATTS; - }; - - union { - // NOLINTNEXTLINE(readability-identifier-naming) - struct gap_event { - esp_gap_ble_cb_event_t gap_event; - esp_ble_gap_cb_param_t gap_param; - } gap; - - // NOLINTNEXTLINE(readability-identifier-naming) - struct gattc_event { - esp_gattc_cb_event_t gattc_event; - esp_gatt_if_t gattc_if; - esp_ble_gattc_cb_param_t gattc_param; - uint8_t data[64]; - } gattc; - - // NOLINTNEXTLINE(readability-identifier-naming) - struct gatts_event { - esp_gatts_cb_event_t gatts_event; - esp_gatt_if_t gatts_if; - esp_ble_gatts_cb_param_t gatts_param; - uint8_t data[64]; - } gatts; - } event_; - // NOLINTNEXTLINE(readability-identifier-naming) - enum ble_event_t : uint8_t { - GAP, - GATTC, - GATTS, - } type_; -}; - } // namespace esp32_ble } // namespace esphome diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py index d6cbb15dd2..311919dcd4 100644 --- a/esphome/components/esp32_ble_beacon/__init__.py +++ b/esphome/components/esp32_ble_beacon/__init__.py @@ -1,8 +1,9 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_TYPE, CONF_UUID -from esphome.core import CORE +from esphome.const import CONF_ID, CONF_TYPE, CONF_UUID, CONF_TX_POWER +from esphome.core import CORE, TimePeriod from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components import esp32_ble DEPENDENCIES = ["esp32"] CONFLICTS_WITH = ["esp32_ble_tracker"] @@ -12,16 +13,49 @@ ESP32BLEBeacon = esp32_ble_beacon_ns.class_("ESP32BLEBeacon", cg.Component) CONF_MAJOR = "major" CONF_MINOR = "minor" +CONF_MIN_INTERVAL = "min_interval" +CONF_MAX_INTERVAL = "max_interval" +CONF_MEASURED_POWER = "measured_power" -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(ESP32BLEBeacon), - cv.Required(CONF_TYPE): cv.one_of("IBEACON", upper=True), - cv.Required(CONF_UUID): cv.uuid, - cv.Optional(CONF_MAJOR, default=10167): cv.uint16_t, - cv.Optional(CONF_MINOR, default=61958): cv.uint16_t, - } -).extend(cv.COMPONENT_SCHEMA) + +def validate_config(config): + if config[CONF_MIN_INTERVAL] > config.get(CONF_MAX_INTERVAL): + raise cv.Invalid("min_interval must be <= max_interval") + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ESP32BLEBeacon), + cv.Required(CONF_TYPE): cv.one_of("IBEACON", upper=True), + cv.Required(CONF_UUID): cv.uuid, + cv.Optional(CONF_MAJOR, default=10167): cv.uint16_t, + cv.Optional(CONF_MINOR, default=61958): cv.uint16_t, + cv.Optional(CONF_MIN_INTERVAL, default="100ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range( + min=TimePeriod(milliseconds=20), max=TimePeriod(milliseconds=10240) + ), + ), + cv.Optional(CONF_MAX_INTERVAL, default="100ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range( + min=TimePeriod(milliseconds=20), max=TimePeriod(milliseconds=10240) + ), + ), + cv.Optional(CONF_MEASURED_POWER, default=-59): cv.int_range( + min=-128, max=0 + ), + cv.Optional(CONF_TX_POWER, default="3dBm"): cv.All( + cv.decibel, cv.one_of(-12, -9, -6, -3, 0, 3, 6, 9, int=True) + ), + } + ).extend(cv.COMPONENT_SCHEMA), + validate_config, +) + +FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant async def to_code(config): @@ -31,6 +65,10 @@ async def to_code(config): await cg.register_component(var, config) cg.add(var.set_major(config[CONF_MAJOR])) cg.add(var.set_minor(config[CONF_MINOR])) + cg.add(var.set_min_interval(config[CONF_MIN_INTERVAL])) + cg.add(var.set_max_interval(config[CONF_MAX_INTERVAL])) + cg.add(var.set_measured_power(config[CONF_MEASURED_POWER])) + cg.add(var.set_tx_power(config[CONF_TX_POWER])) if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp index 955bc8595f..589fcc1e82 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp @@ -36,11 +36,24 @@ static esp_ble_adv_params_t ble_adv_params = { #define ENDIAN_CHANGE_U16(x) ((((x) &0xFF00) >> 8) + (((x) &0xFF) << 8)) static const esp_ble_ibeacon_head_t IBEACON_COMMON_HEAD = { - .flags = {0x02, 0x01, 0x06}, .length = 0x1A, .type = 0xFF, .company_id = 0x004C, .beacon_type = 0x1502}; + .flags = {0x02, 0x01, 0x06}, .length = 0x1A, .type = 0xFF, .company_id = {0x4C, 0x00}, .beacon_type = {0x02, 0x15}}; void ESP32BLEBeacon::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE Beacon:"); - ESP_LOGCONFIG(TAG, " Major: %u, Minor: %u", this->major_, this->minor_); + char uuid[37]; + char *bpos = uuid; + for (int8_t ii = 0; ii < 16; ++ii) { + bpos += sprintf(bpos, "%02X", this->uuid_[ii]); + if (ii == 3 || ii == 5 || ii == 7 || ii == 9) { + bpos += sprintf(bpos, "-"); + } + } + uuid[36] = '\0'; + ESP_LOGCONFIG(TAG, + " UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d" + ", TX Power: %ddBm", + uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_, + this->tx_power_); } void ESP32BLEBeacon::setup() { @@ -67,6 +80,9 @@ void ESP32BLEBeacon::ble_core_task(void *params) { } void ESP32BLEBeacon::ble_setup() { + ble_adv_params.adv_int_min = static_cast(global_esp32_ble_beacon->min_interval_ / 0.625f); + ble_adv_params.adv_int_max = static_cast(global_esp32_ble_beacon->max_interval_ / 0.625f); + // Initialize non-volatile storage for the bluetooth controller esp_err_t err = nvs_flash_init(); if (err != ESP_OK) { @@ -118,6 +134,12 @@ void ESP32BLEBeacon::ble_setup() { ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err); return; } + err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, + static_cast((global_esp32_ble_beacon->tx_power_ + 12) / 3)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err)); + return; + } err = esp_ble_gap_register_callback(ESP32BLEBeacon::gap_event_handler); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); @@ -130,7 +152,7 @@ void ESP32BLEBeacon::ble_setup() { sizeof(ibeacon_adv_data.ibeacon_vendor.proximity_uuid)); ibeacon_adv_data.ibeacon_vendor.minor = ENDIAN_CHANGE_U16(global_esp32_ble_beacon->minor_); ibeacon_adv_data.ibeacon_vendor.major = ENDIAN_CHANGE_U16(global_esp32_ble_beacon->major_); - ibeacon_adv_data.ibeacon_vendor.measured_power = 0xC5; + ibeacon_adv_data.ibeacon_vendor.measured_power = static_cast(global_esp32_ble_beacon->measured_power_); esp_ble_gap_config_adv_data_raw((uint8_t *) &ibeacon_adv_data, sizeof(ibeacon_adv_data)); } @@ -153,7 +175,7 @@ void ESP32BLEBeacon::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap break; } case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: { - err = param->adv_start_cmpl.status; + err = param->adv_stop_cmpl.status; if (err != ESP_BT_STATUS_SUCCESS) { ESP_LOGE(TAG, "BLE adv stop failed: %s", esp_err_to_name(err)); } else { diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h index 80ad2041f2..5208b67ea3 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h @@ -5,6 +5,7 @@ #ifdef USE_ESP32 #include +#include namespace esphome { namespace esp32_ble_beacon { @@ -14,8 +15,8 @@ typedef struct { uint8_t flags[3]; uint8_t length; uint8_t type; - uint16_t company_id; - uint16_t beacon_type; + uint8_t company_id[2]; + uint8_t beacon_type[2]; } __attribute__((packed)) esp_ble_ibeacon_head_t; // NOLINTNEXTLINE(modernize-use-using) @@ -42,6 +43,10 @@ class ESP32BLEBeacon : public Component { void set_major(uint16_t major) { this->major_ = major; } void set_minor(uint16_t minor) { this->minor_ = minor; } + void set_min_interval(uint16_t val) { this->min_interval_ = val; } + void set_max_interval(uint16_t val) { this->max_interval_ = val; } + void set_measured_power(int8_t val) { this->measured_power_ = val; } + void set_tx_power(int8_t val) { this->tx_power_ = val; } protected: static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); @@ -51,6 +56,10 @@ class ESP32BLEBeacon : public Component { std::array uuid_; uint16_t major_{}; uint16_t minor_{}; + uint16_t min_interval_{}; + uint16_t max_interval_{}; + int8_t measured_power_{}; + int8_t tx_power_{}; }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/esp32_ble_client/__init__.py b/esphome/components/esp32_ble_client/__init__.py new file mode 100644 index 0000000000..94a5576d0b --- /dev/null +++ b/esphome/components/esp32_ble_client/__init__.py @@ -0,0 +1,12 @@ +import esphome.codegen as cg + +from esphome.components import esp32_ble_tracker + +AUTO_LOAD = ["esp32_ble_tracker"] +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["esp32"] + +esp32_ble_client_ns = cg.esphome_ns.namespace("esp32_ble_client") +BLEClientBase = esp32_ble_client_ns.class_( + "BLEClientBase", esp32_ble_tracker.ESPBTClient, cg.Component +) diff --git a/esphome/components/esp32_ble_client/ble_characteristic.cpp b/esphome/components/esp32_ble_client/ble_characteristic.cpp new file mode 100644 index 0000000000..2fd7fe9871 --- /dev/null +++ b/esphome/components/esp32_ble_client/ble_characteristic.cpp @@ -0,0 +1,99 @@ +#include "ble_characteristic.h" +#include "ble_client_base.h" +#include "ble_service.h" + +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_client { + +static const char *const TAG = "esp32_ble_client"; + +BLECharacteristic::~BLECharacteristic() { + for (auto &desc : this->descriptors) + delete desc; // NOLINT(cppcoreguidelines-owning-memory) +} + +void BLECharacteristic::release_descriptors() { + this->parsed = false; + for (auto &desc : this->descriptors) + delete desc; // NOLINT(cppcoreguidelines-owning-memory) + this->descriptors.clear(); +} + +void BLECharacteristic::parse_descriptors() { + this->parsed = true; + uint16_t offset = 0; + esp_gattc_descr_elem_t result; + + while (true) { + uint16_t count = 1; + esp_gatt_status_t status = + esp_ble_gattc_get_all_descr(this->service->client->get_gattc_if(), this->service->client->get_conn_id(), + this->handle, &result, &count, offset); + if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) { + break; + } + if (status != ESP_GATT_OK) { + ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", + this->service->client->get_connection_index(), this->service->client->address_str().c_str(), status); + break; + } + if (count == 0) { + break; + } + + BLEDescriptor *desc = new BLEDescriptor(); // NOLINT(cppcoreguidelines-owning-memory) + desc->uuid = espbt::ESPBTUUID::from_uuid(result.uuid); + desc->handle = result.handle; + desc->characteristic = this; + this->descriptors.push_back(desc); + ESP_LOGV(TAG, "[%d] [%s] descriptor %s, handle 0x%x", this->service->client->get_connection_index(), + this->service->client->address_str().c_str(), desc->uuid.to_string().c_str(), desc->handle); + offset++; + } +} + +BLEDescriptor *BLECharacteristic::get_descriptor(espbt::ESPBTUUID uuid) { + if (!this->parsed) + this->parse_descriptors(); + for (auto &desc : this->descriptors) { + if (desc->uuid == uuid) + return desc; + } + return nullptr; +} +BLEDescriptor *BLECharacteristic::get_descriptor(uint16_t uuid) { + return this->get_descriptor(espbt::ESPBTUUID::from_uint16(uuid)); +} +BLEDescriptor *BLECharacteristic::get_descriptor_by_handle(uint16_t handle) { + if (!this->parsed) + this->parse_descriptors(); + for (auto &desc : this->descriptors) { + if (desc->handle == handle) + return desc; + } + return nullptr; +} + +esp_err_t BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size, esp_gatt_write_type_t write_type) { + auto *client = this->service->client; + auto status = esp_ble_gattc_write_char(client->get_gattc_if(), client->get_conn_id(), this->handle, new_val_size, + new_val, write_type, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "[%d] [%s] Error sending write value to BLE gattc server, status=%d", + this->service->client->get_connection_index(), this->service->client->address_str().c_str(), status); + } + return status; +} + +esp_err_t BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size) { + return write_value(new_val, new_val_size, ESP_GATT_WRITE_TYPE_NO_RSP); +} + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_characteristic.h b/esphome/components/esp32_ble_client/ble_characteristic.h new file mode 100644 index 0000000000..a014788e65 --- /dev/null +++ b/esphome/components/esp32_ble_client/ble_characteristic.h @@ -0,0 +1,39 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#include "ble_descriptor.h" + +#include + +namespace esphome { +namespace esp32_ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEService; + +class BLECharacteristic { + public: + ~BLECharacteristic(); + bool parsed = false; + espbt::ESPBTUUID uuid; + uint16_t handle; + esp_gatt_char_prop_t properties; + std::vector descriptors; + void parse_descriptors(); + void release_descriptors(); + BLEDescriptor *get_descriptor(espbt::ESPBTUUID uuid); + BLEDescriptor *get_descriptor(uint16_t uuid); + BLEDescriptor *get_descriptor_by_handle(uint16_t handle); + esp_err_t write_value(uint8_t *new_val, int16_t new_val_size); + esp_err_t write_value(uint8_t *new_val, int16_t new_val_size, esp_gatt_write_type_t write_type); + BLEService *service; +}; + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp new file mode 100644 index 0000000000..40eff49266 --- /dev/null +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -0,0 +1,418 @@ +#include "ble_client_base.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_client { + +static const char *const TAG = "esp32_ble_client"; +static const esp_bt_uuid_t NOTIFY_DESC_UUID = { + .len = ESP_UUID_LEN_16, + .uuid = + { + .uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG, + }, +}; + +void BLEClientBase::setup() { + static uint8_t connection_index = 0; + this->connection_index_ = connection_index++; + + auto ret = esp_ble_gattc_app_register(this->app_id); + if (ret) { + ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret); + this->mark_failed(); + } + this->set_state(espbt::ClientState::IDLE); +} + +void BLEClientBase::loop() { + // READY_TO_CONNECT means we have discovered the device + // and the scanner has been stopped by the tracker. + if (this->state_ == espbt::ClientState::READY_TO_CONNECT) { + this->connect(); + } +} + +float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } + +bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { + if (this->address_ == 0 || device.address_uint64() != this->address_) + return false; + if (this->state_ != espbt::ClientState::IDLE && this->state_ != espbt::ClientState::SEARCHING) + return false; + + ESP_LOGD(TAG, "[%d] [%s] Found device", this->connection_index_, this->address_str_.c_str()); + this->set_state(espbt::ClientState::DISCOVERED); + + auto addr = device.address_uint64(); + this->remote_bda_[0] = (addr >> 40) & 0xFF; + this->remote_bda_[1] = (addr >> 32) & 0xFF; + this->remote_bda_[2] = (addr >> 24) & 0xFF; + this->remote_bda_[3] = (addr >> 16) & 0xFF; + this->remote_bda_[4] = (addr >> 8) & 0xFF; + this->remote_bda_[5] = (addr >> 0) & 0xFF; + this->remote_addr_type_ = device.get_address_type(); + return true; +} + +void BLEClientBase::connect() { + ESP_LOGI(TAG, "[%d] [%s] 0x%02x Attempting BLE connection", this->connection_index_, this->address_str_.c_str(), + this->remote_addr_type_); + this->paired_ = false; + auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true); + if (ret) { + ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_open error, status=%d", this->connection_index_, this->address_str_.c_str(), + ret); + this->set_state(espbt::ClientState::IDLE); + } else { + this->set_state(espbt::ClientState::CONNECTING); + } +} + +esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); } + +void BLEClientBase::disconnect() { + if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) + return; + ESP_LOGI(TAG, "[%d] [%s] Disconnecting.", this->connection_index_, this->address_str_.c_str()); + auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_close error, err=%d", this->connection_index_, this->address_str_.c_str(), + err); + } + + if (this->state_ == espbt::ClientState::SEARCHING || this->state_ == espbt::ClientState::READY_TO_CONNECT || + this->state_ == espbt::ClientState::DISCOVERED) { + this->set_address(0); + this->set_state(espbt::ClientState::IDLE); + } else { + this->set_state(espbt::ClientState::DISCONNECTING); + } +} + +void BLEClientBase::release_services() { + for (auto &svc : this->services_) + delete svc; // NOLINT(cppcoreguidelines-owning-memory) + this->services_.clear(); +#ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH + esp_ble_gattc_cache_clean(this->remote_bda_); +#endif +} + +bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, + esp_ble_gattc_cb_param_t *param) { + if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id) + return false; + if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && esp_gattc_if != this->gattc_if_) + return false; + + ESP_LOGV(TAG, "[%d] [%s] gattc_event_handler: event=%d gattc_if=%d", this->connection_index_, + this->address_str_.c_str(), event, esp_gattc_if); + + switch (event) { + case ESP_GATTC_REG_EVT: { + if (param->reg.status == ESP_GATT_OK) { + ESP_LOGV(TAG, "[%d] [%s] gattc registered app id %d", this->connection_index_, this->address_str_.c_str(), + this->app_id); + this->gattc_if_ = esp_gattc_if; + } else { + ESP_LOGE(TAG, "[%d] [%s] gattc app registration failed id=%d code=%d", this->connection_index_, + this->address_str_.c_str(), param->reg.app_id, param->reg.status); + } + break; + } + case ESP_GATTC_OPEN_EVT: { + ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT", this->connection_index_, this->address_str_.c_str()); + this->conn_id_ = param->open.conn_id; + this->service_count_ = 0; + if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { + ESP_LOGW(TAG, "[%d] [%s] Connection failed, status=%d", this->connection_index_, this->address_str_.c_str(), + param->open.status); + this->set_state(espbt::ClientState::IDLE); + break; + } + auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->open.conn_id); + if (ret) { + ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_, + this->address_str_.c_str(), ret); + } + if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { + ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str()); + this->set_state(espbt::ClientState::CONNECTED); + this->state_ = espbt::ClientState::ESTABLISHED; + break; + } + esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr); + break; + } + case ESP_GATTC_CFG_MTU_EVT: { + if (param->cfg_mtu.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_, + this->address_str_.c_str(), param->cfg_mtu.mtu, param->cfg_mtu.status); + this->set_state(espbt::ClientState::IDLE); + break; + } + ESP_LOGV(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_.c_str(), + param->cfg_mtu.status, param->cfg_mtu.mtu); + this->mtu_ = param->cfg_mtu.mtu; + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + if (memcmp(param->disconnect.remote_bda, this->remote_bda_, 6) != 0) + return false; + ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->connection_index_, + this->address_str_.c_str(), param->disconnect.reason); + this->release_services(); + this->set_state(espbt::ClientState::IDLE); + break; + } + case ESP_GATTC_SEARCH_RES_EVT: { + this->service_count_++; + if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { + // V3 clients don't need services initialized since + // they only request by handle after receiving the services. + break; + } + BLEService *ble_service = new BLEService(); // NOLINT(cppcoreguidelines-owning-memory) + ble_service->uuid = espbt::ESPBTUUID::from_uuid(param->search_res.srvc_id.uuid); + ble_service->start_handle = param->search_res.start_handle; + ble_service->end_handle = param->search_res.end_handle; + ble_service->client = this; + this->services_.push_back(ble_service); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_SEARCH_CMPL_EVT", this->connection_index_, this->address_str_.c_str()); + for (auto &svc : this->services_) { + ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(), + svc->uuid.to_string().c_str()); + ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, + this->address_str_.c_str(), svc->start_handle, svc->end_handle); + } + ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str()); + this->set_state(espbt::ClientState::CONNECTED); + this->state_ = espbt::ClientState::ESTABLISHED; + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || + this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { + // Client is responsible for flipping the descriptor value + // when using the cache + break; + } + esp_gattc_descr_elem_t desc_result; + uint16_t count = 1; + esp_gatt_status_t descr_status = + esp_ble_gattc_get_descr_by_char_handle(this->gattc_if_, this->connection_index_, param->reg_for_notify.handle, + NOTIFY_DESC_UUID, &desc_result, &count); + if (descr_status != ESP_GATT_OK) { + ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_descr_by_char_handle error, status=%d", this->connection_index_, + this->address_str_.c_str(), descr_status); + break; + } + esp_gattc_char_elem_t char_result; + esp_gatt_status_t char_status = + esp_ble_gattc_get_all_char(this->gattc_if_, this->connection_index_, param->reg_for_notify.handle, + param->reg_for_notify.handle, &char_result, &count, 0); + if (char_status != ESP_GATT_OK) { + ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_, + this->address_str_.c_str(), char_status); + break; + } + + /* + 1 = notify + 2 = indicate + */ + uint16_t notify_en = char_result.properties & ESP_GATT_CHAR_PROP_BIT_NOTIFY ? 1 : 2; + esp_err_t status = + esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, desc_result.handle, sizeof(notify_en), + (uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, status=%d", this->connection_index_, + this->address_str_.c_str(), status); + } + break; + } + + default: + break; + } + return true; +} + +void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + // This event is sent by the server when it requests security + case ESP_GAP_BLE_SEC_REQ_EVT: + if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0) + break; + ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_.c_str(), event); + esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); + break; + // This event is sent once authentication has completed + case ESP_GAP_BLE_AUTH_CMPL_EVT: + if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0) + break; + esp_bd_addr_t bd_addr; + memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t)); + ESP_LOGI(TAG, "[%d] [%s] auth complete. remote BD_ADDR: %s", this->connection_index_, this->address_str_.c_str(), + format_hex(bd_addr, 6).c_str()); + if (!param->ble_security.auth_cmpl.success) { + ESP_LOGE(TAG, "[%d] [%s] auth fail reason = 0x%x", this->connection_index_, this->address_str_.c_str(), + param->ble_security.auth_cmpl.fail_reason); + } else { + this->paired_ = true; + ESP_LOGV(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_, + this->address_str_.c_str(), param->ble_security.auth_cmpl.addr_type, + param->ble_security.auth_cmpl.auth_mode); + } + break; + // There are other events we'll want to implement at some point to support things like pass key + // https://github.com/espressif/esp-idf/blob/cba69dd088344ed9d26739f04736ae7a37541b3a/examples/bluetooth/bluedroid/ble/gatt_security_client/tutorial/Gatt_Security_Client_Example_Walkthrough.md + default: + break; + } +} + +// Parse GATT values into a float for a sensor. +// Ref: https://www.bluetooth.com/specifications/assigned-numbers/format-types/ +float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { + // A length of one means a single octet value. + if (length == 0) + return 0; + if (length == 1) + return (float) ((uint8_t) value[0]); + + switch (value[0]) { + case 0x1: // boolean. + case 0x2: // 2bit. + case 0x3: // nibble. + case 0x4: // uint8. + return (float) ((uint8_t) value[1]); + case 0x5: // uint12. + case 0x6: // uint16. + if (length > 2) { + return (float) encode_uint16(value[1], value[2]); + } + // fall through + case 0x7: // uint24. + if (length > 3) { + return (float) encode_uint24(value[1], value[2], value[3]); + } + // fall through + case 0x8: // uint32. + if (length > 4) { + return (float) encode_uint32(value[1], value[2], value[3], value[4]); + } + // fall through + case 0xC: // int8. + return (float) ((int8_t) value[1]); + case 0xD: // int12. + case 0xE: // int16. + if (length > 2) { + return (float) ((int16_t) (value[1] << 8) + (int16_t) value[2]); + } + // fall through + case 0xF: // int24. + if (length > 3) { + return (float) ((int32_t) (value[1] << 16) + (int32_t) (value[2] << 8) + (int32_t) (value[3])); + } + // fall through + case 0x10: // int32. + if (length > 4) { + return (float) ((int32_t) (value[1] << 24) + (int32_t) (value[2] << 16) + (int32_t) (value[3] << 8) + + (int32_t) (value[4])); + } + } + ESP_LOGW(TAG, "[%d] [%s] Cannot parse characteristic value of type 0x%x length %d", this->connection_index_, + this->address_str_.c_str(), value[0], length); + return NAN; +} + +BLEService *BLEClientBase::get_service(espbt::ESPBTUUID uuid) { + for (auto *svc : this->services_) { + if (svc->uuid == uuid) + return svc; + } + return nullptr; +} + +BLEService *BLEClientBase::get_service(uint16_t uuid) { return this->get_service(espbt::ESPBTUUID::from_uint16(uuid)); } + +BLECharacteristic *BLEClientBase::get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr) { + auto *svc = this->get_service(service); + if (svc == nullptr) + return nullptr; + return svc->get_characteristic(chr); +} + +BLECharacteristic *BLEClientBase::get_characteristic(uint16_t service, uint16_t chr) { + return this->get_characteristic(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr)); +} + +BLECharacteristic *BLEClientBase::get_characteristic(uint16_t handle) { + for (auto *svc : this->services_) { + if (!svc->parsed) + svc->parse_characteristics(); + for (auto *chr : svc->characteristics) { + if (chr->handle == handle) + return chr; + } + } + return nullptr; +} + +BLEDescriptor *BLEClientBase::get_config_descriptor(uint16_t handle) { + auto *chr = this->get_characteristic(handle); + if (chr != nullptr) { + if (!chr->parsed) + chr->parse_descriptors(); + for (auto &desc : chr->descriptors) { + if (desc->uuid.get_uuid().uuid.uuid16 == ESP_GATT_UUID_CHAR_CLIENT_CONFIG) + return desc; + } + } + return nullptr; +} + +BLEDescriptor *BLEClientBase::get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr) { + auto *svc = this->get_service(service); + if (svc == nullptr) + return nullptr; + auto *ch = svc->get_characteristic(chr); + if (ch == nullptr) + return nullptr; + return ch->get_descriptor(descr); +} + +BLEDescriptor *BLEClientBase::get_descriptor(uint16_t service, uint16_t chr, uint16_t descr) { + return this->get_descriptor(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr), + espbt::ESPBTUUID::from_uint16(descr)); +} + +BLEDescriptor *BLEClientBase::get_descriptor(uint16_t handle) { + for (auto *svc : this->services_) { + if (!svc->parsed) + svc->parse_characteristics(); + for (auto *chr : svc->characteristics) { + if (!chr->parsed) + chr->parse_descriptors(); + for (auto *desc : chr->descriptors) { + if (desc->handle == handle) + return desc; + } + } + } + return nullptr; +} + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h new file mode 100644 index 0000000000..97886d0b19 --- /dev/null +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -0,0 +1,101 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/core/component.h" + +#include "ble_service.h" + +#include +#include +#include + +#include +#include +#include +#include + +namespace esphome { +namespace esp32_ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEClientBase : public espbt::ESPBTClient, public Component { + public: + void setup() override; + void loop() override; + float get_setup_priority() const override; + + bool parse_device(const espbt::ESPBTDevice &device) override; + void on_scan_end() override {} + bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; + void connect() override; + esp_err_t pair(); + void disconnect(); + void release_services(); + + bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; } + + void set_address(uint64_t address) { + this->address_ = address; + if (address == 0) { + memset(this->remote_bda_, 0, sizeof(this->remote_bda_)); + this->address_str_ = ""; + } else { + this->address_str_ = + str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, (uint8_t) (this->address_ >> 40) & 0xff, + (uint8_t) (this->address_ >> 32) & 0xff, (uint8_t) (this->address_ >> 24) & 0xff, + (uint8_t) (this->address_ >> 16) & 0xff, (uint8_t) (this->address_ >> 8) & 0xff, + (uint8_t) (this->address_ >> 0) & 0xff); + } + } + std::string address_str() const { return this->address_str_; } + + BLEService *get_service(espbt::ESPBTUUID uuid); + BLEService *get_service(uint16_t uuid); + BLECharacteristic *get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr); + BLECharacteristic *get_characteristic(uint16_t service, uint16_t chr); + BLECharacteristic *get_characteristic(uint16_t handle); + BLEDescriptor *get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr); + BLEDescriptor *get_descriptor(uint16_t service, uint16_t chr, uint16_t descr); + BLEDescriptor *get_descriptor(uint16_t handle); + // Get the configuration descriptor for the given characteristic handle. + BLEDescriptor *get_config_descriptor(uint16_t handle); + + float parse_char_value(uint8_t *value, uint16_t length); + + int get_gattc_if() const { return this->gattc_if_; } + uint8_t *get_remote_bda() { return this->remote_bda_; } + esp_ble_addr_type_t get_remote_addr_type() const { return this->remote_addr_type_; } + void set_remote_addr_type(esp_ble_addr_type_t address_type) { this->remote_addr_type_ = address_type; } + uint16_t get_conn_id() const { return this->conn_id_; } + uint64_t get_address() const { return this->address_; } + bool is_paired() const { return this->paired_; } + + uint8_t get_connection_index() const { return this->connection_index_; } + + virtual void set_connection_type(espbt::ConnectionType ct) { this->connection_type_ = ct; } + + protected: + int gattc_if_; + esp_bd_addr_t remote_bda_; + esp_ble_addr_type_t remote_addr_type_; + uint16_t conn_id_{0xFFFF}; + uint64_t address_{0}; + std::string address_str_{}; + uint8_t connection_index_; + int16_t service_count_{0}; + uint16_t mtu_{23}; + bool paired_{false}; + espbt::ConnectionType connection_type_{espbt::ConnectionType::V1}; + + std::vector services_; +}; + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_descriptor.h b/esphome/components/esp32_ble_client/ble_descriptor.h new file mode 100644 index 0000000000..c05430144f --- /dev/null +++ b/esphome/components/esp32_ble_client/ble_descriptor.h @@ -0,0 +1,25 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +namespace esphome { +namespace esp32_ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLECharacteristic; + +class BLEDescriptor { + public: + espbt::ESPBTUUID uuid; + uint16_t handle; + + BLECharacteristic *characteristic; +}; + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_service.cpp b/esphome/components/esp32_ble_client/ble_service.cpp new file mode 100644 index 0000000000..b22d2a1788 --- /dev/null +++ b/esphome/components/esp32_ble_client/ble_service.cpp @@ -0,0 +1,77 @@ +#include "ble_service.h" +#include "ble_client_base.h" + +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_client { + +static const char *const TAG = "esp32_ble_client"; + +BLECharacteristic *BLEService::get_characteristic(espbt::ESPBTUUID uuid) { + if (!this->parsed) + this->parse_characteristics(); + for (auto &chr : this->characteristics) { + if (chr->uuid == uuid) + return chr; + } + return nullptr; +} + +BLECharacteristic *BLEService::get_characteristic(uint16_t uuid) { + return this->get_characteristic(espbt::ESPBTUUID::from_uint16(uuid)); +} + +BLEService::~BLEService() { + for (auto &chr : this->characteristics) + delete chr; // NOLINT(cppcoreguidelines-owning-memory) +} + +void BLEService::release_characteristics() { + this->parsed = false; + for (auto &chr : this->characteristics) + delete chr; // NOLINT(cppcoreguidelines-owning-memory) + this->characteristics.clear(); +} + +void BLEService::parse_characteristics() { + this->parsed = true; + uint16_t offset = 0; + esp_gattc_char_elem_t result; + + while (true) { + uint16_t count = 1; + esp_gatt_status_t status = + esp_ble_gattc_get_all_char(this->client->get_gattc_if(), this->client->get_conn_id(), this->start_handle, + this->end_handle, &result, &count, offset); + if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) { + break; + } + if (status != ESP_GATT_OK) { + ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->client->get_connection_index(), + this->client->address_str().c_str(), status); + break; + } + if (count == 0) { + break; + } + + BLECharacteristic *characteristic = new BLECharacteristic(); // NOLINT(cppcoreguidelines-owning-memory) + characteristic->uuid = espbt::ESPBTUUID::from_uuid(result.uuid); + characteristic->properties = result.properties; + characteristic->handle = result.char_handle; + characteristic->service = this; + this->characteristics.push_back(characteristic); + ESP_LOGV(TAG, "[%d] [%s] characteristic %s, handle 0x%x, properties 0x%x", this->client->get_connection_index(), + this->client->address_str().c_str(), characteristic->uuid.to_string().c_str(), characteristic->handle, + characteristic->properties); + offset++; + } +} + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_service.h b/esphome/components/esp32_ble_client/ble_service.h new file mode 100644 index 0000000000..41fc3e838b --- /dev/null +++ b/esphome/components/esp32_ble_client/ble_service.h @@ -0,0 +1,36 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#include "ble_characteristic.h" + +#include + +namespace esphome { +namespace esp32_ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEClientBase; + +class BLEService { + public: + ~BLEService(); + bool parsed = false; + espbt::ESPBTUUID uuid; + uint16_t start_handle; + uint16_t end_handle; + std::vector characteristics; + BLEClientBase *client; + void parse_characteristics(); + void release_characteristics(); + BLECharacteristic *get_characteristic(espbt::ESPBTUUID uuid); + BLECharacteristic *get_characteristic(uint16_t uuid); +}; + +} // namespace esp32_ble_client +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 2fcc5c7743..0ddfa62c1b 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -7,21 +7,25 @@ from esphome.components.esp32 import add_idf_sdkconfig_option AUTO_LOAD = ["esp32_ble"] CODEOWNERS = ["@jesserockz"] -CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"] +CONFLICTS_WITH = ["esp32_ble_beacon"] DEPENDENCIES = ["esp32"] CONF_MANUFACTURER = "manufacturer" -CONF_BLE_ID = "ble_id" esp32_ble_server_ns = cg.esphome_ns.namespace("esp32_ble_server") -BLEServer = esp32_ble_server_ns.class_("BLEServer", cg.Component) +BLEServer = esp32_ble_server_ns.class_( + "BLEServer", + cg.Component, + esp32_ble.GATTsEventHandler, + cg.Parented.template(esp32_ble.ESP32BLE), +) BLEServiceComponent = esp32_ble_server_ns.class_("BLEServiceComponent") CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(BLEServer), - cv.GenerateID(CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), + cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), cv.Optional(CONF_MANUFACTURER, default="ESPHome"): cv.string, cv.Optional(CONF_MODEL): cv.string, } @@ -29,16 +33,18 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): - parent = await cg.get_variable(config[CONF_BLE_ID]) var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) + cg.add(parent.register_gatts_event_handler(var)) + cg.add(var.set_parent(parent)) + cg.add(var.set_manufacturer(config[CONF_MANUFACTURER])) if CONF_MODEL in config: cg.add(var.set_model(config[CONF_MODEL])) cg.add_define("USE_ESP32_BLE_SERVER") - cg.add(parent.set_server(var)) - if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index df822ac0b9..15a51f6ede 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -148,44 +148,44 @@ bool BLECharacteristic::is_failed() { void BLECharacteristic::set_broadcast_property(bool value) { if (value) { - this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_BROADCAST); + this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_BROADCAST); } else { - this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_BROADCAST); + this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_BROADCAST); } } void BLECharacteristic::set_indicate_property(bool value) { if (value) { - this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_INDICATE); + this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_INDICATE); } else { - this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_INDICATE); + this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_INDICATE); } } void BLECharacteristic::set_notify_property(bool value) { if (value) { - this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_NOTIFY); + this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_NOTIFY); } else { - this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_NOTIFY); + this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_NOTIFY); } } void BLECharacteristic::set_read_property(bool value) { if (value) { - this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_READ); + this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_READ); } else { - this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_READ); + this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_READ); } } void BLECharacteristic::set_write_property(bool value) { if (value) { - this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE); + this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE); } else { - this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE); + this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE); } } void BLECharacteristic::set_write_no_response_property(bool value) { if (value) { - this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE_NR); + this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE_NR); } else { - this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE_NR); + this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE_NR); } } diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 15bea07021..7cbf40c076 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -25,7 +25,8 @@ static const uint16_t VERSION_UUID = 0x2A26; static const uint16_t MANUFACTURER_UUID = 0x2A29; void BLEServer::setup() { - if (this->is_failed()) { + if (this->parent_->is_failed()) { + this->mark_failed(); ESP_LOGE(TAG, "BLE Server was marked failed by ESP32BLE"); return; } diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index d275eeab01..ac759f2dcd 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -3,6 +3,7 @@ #include "ble_service.h" #include "ble_characteristic.h" +#include "esphome/components/esp32_ble/ble.h" #include "esphome/components/esp32_ble/ble_advertising.h" #include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/queue.h" @@ -12,6 +13,7 @@ #include #include +#include #ifdef USE_ESP32 @@ -31,7 +33,7 @@ class BLEServiceComponent { virtual void stop(); }; -class BLEServer : public Component { +class BLEServer : public Component, public GATTsEventHandler, public Parented { public: void setup() override; void loop() override; @@ -54,7 +56,8 @@ class BLEServer : public Component { uint32_t get_connected_client_count() { return this->connected_clients_; } const std::map &get_clients() { return this->clients_; } - void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); + void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, + esp_ble_gatts_cb_param_t *param) override; void register_service_component(BLEServiceComponent *component) { this->service_components_.push_back(component); } diff --git a/esphome/components/esp32_ble_server/ble_service.h b/esphome/components/esp32_ble_server/ble_service.h index 16cc897238..2766c931a7 100644 --- a/esphome/components/esp32_ble_server/ble_service.h +++ b/esphome/components/esp32_ble_server/ble_service.h @@ -3,6 +3,8 @@ #include "ble_characteristic.h" #include "esphome/components/esp32_ble/ble_uuid.h" +#include + #ifdef USE_ESP32 #include diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index e647b74a8f..30589f1a3f 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.const import ( + CONF_ACTIVE, CONF_ID, CONF_INTERVAL, CONF_DURATION, @@ -14,17 +15,26 @@ from esphome.const import ( CONF_ON_BLE_SERVICE_DATA_ADVERTISE, CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, ) +from esphome.components import esp32_ble from esphome.core import CORE from esphome.components.esp32 import add_idf_sdkconfig_option +AUTO_LOAD = ["esp32_ble"] DEPENDENCIES = ["esp32"] CONF_ESP32_BLE_ID = "esp32_ble_id" CONF_SCAN_PARAMETERS = "scan_parameters" CONF_WINDOW = "window" -CONF_ACTIVE = "active" +CONF_CONTINUOUS = "continuous" +CONF_ON_SCAN_END = "on_scan_end" esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") -ESP32BLETracker = esp32_ble_tracker_ns.class_("ESP32BLETracker", cg.Component) +ESP32BLETracker = esp32_ble_tracker_ns.class_( + "ESP32BLETracker", + cg.Component, + esp32_ble.GAPEventHandler, + esp32_ble.GATTcEventHandler, + cg.Parented.template(esp32_ble.ESP32BLE), +) ESPBTClient = esp32_ble_tracker_ns.class_("ESPBTClient") ESPBTDeviceListener = esp32_ble_tracker_ns.class_("ESPBTDeviceListener") ESPBTDevice = esp32_ble_tracker_ns.class_("ESPBTDevice") @@ -42,6 +52,16 @@ BLEManufacturerDataAdvertiseTrigger = esp32_ble_tracker_ns.class_( "BLEManufacturerDataAdvertiseTrigger", automation.Trigger.template(adv_data_t_const_ref), ) +BLEEndOfScanTrigger = esp32_ble_tracker_ns.class_( + "BLEEndOfScanTrigger", automation.Trigger.template() +) +# Actions +ESP32BLEStartScanAction = esp32_ble_tracker_ns.class_( + "ESP32BLEStartScanAction", automation.Action +) +ESP32BLEStopScanAction = esp32_ble_tracker_ns.class_( + "ESP32BLEStopScanAction", automation.Action +) def validate_scan_parameters(config): @@ -125,6 +145,7 @@ def as_reversed_hex_array(value): CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32BLETracker), + cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All( cv.Schema( { @@ -138,6 +159,7 @@ CONFIG_SCHEMA = cv.Schema( CONF_WINDOW, default="30ms" ): cv.positive_time_period_milliseconds, cv.Optional(CONF_ACTIVE, default=True): cv.boolean, + cv.Optional(CONF_CONTINUOUS, default=True): cv.boolean, } ), validate_scan_parameters, @@ -145,7 +167,7 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_ON_BLE_ADVERTISE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPBTAdvertiseTrigger), - cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_MAC_ADDRESS): cv.ensure_list(cv.mac_address), } ), cv.Optional(CONF_ON_BLE_SERVICE_DATA_ADVERTISE): automation.validate_automation( @@ -168,9 +190,14 @@ CONFIG_SCHEMA = cv.Schema( cv.Required(CONF_MANUFACTURER_ID): bt_uuid, } ), + cv.Optional(CONF_ON_SCAN_END): automation.validate_automation( + {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BLEEndOfScanTrigger)} + ), } ).extend(cv.COMPONENT_SCHEMA) +FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant + ESP_BLE_DEVICE_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ESP32_BLE_ID): cv.use_id(ESP32BLETracker), @@ -181,15 +208,25 @@ ESP_BLE_DEVICE_SCHEMA = cv.Schema( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + + parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) + cg.add(parent.register_gap_event_handler(var)) + cg.add(parent.register_gattc_event_handler(var)) + cg.add(var.set_parent(parent)) + params = config[CONF_SCAN_PARAMETERS] cg.add(var.set_scan_duration(params[CONF_DURATION])) cg.add(var.set_scan_interval(int(params[CONF_INTERVAL].total_milliseconds / 0.625))) cg.add(var.set_scan_window(int(params[CONF_WINDOW].total_milliseconds / 0.625))) cg.add(var.set_scan_active(params[CONF_ACTIVE])) + cg.add(var.set_scan_continuous(params[CONF_CONTINUOUS])) for conf in config.get(CONF_ON_BLE_ADVERTISE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) if CONF_MAC_ADDRESS in conf: - cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) + addr_list = [] + for it in conf[CONF_MAC_ADDRESS]: + addr_list.append(it.as_hex) + cg.add(trigger.set_addresses(addr_list)) await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf) for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) @@ -215,9 +252,64 @@ async def to_code(config): if CONF_MAC_ADDRESS in conf: cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf) + for conf in config.get(CONF_ON_SCAN_END, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) + # https://github.com/espressif/esp-idf/issues/4101 + # https://github.com/espressif/esp-idf/issues/2503 + # Match arduino CONFIG_BTU_TASK_STACK_SIZE + # https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866 + add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192) + + cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts + cg.add_define("USE_ESP32_BLE_CLIENT") + + +ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(ESP32BLETracker), + cv.Optional(CONF_CONTINUOUS, default=False): cv.templatable(cv.boolean), + } +) + + +@automation.register_action( + "esp32_ble_tracker.start_scan", + ESP32BLEStartScanAction, + ESP32_BLE_START_SCAN_ACTION_SCHEMA, +) +async def esp32_ble_tracker_start_scan_action_to_code( + config, action_id, template_arg, args +): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + cg.add(var.set_continuous(config[CONF_CONTINUOUS])) + return var + + +ESP32_BLE_STOP_SCAN_ACTION_SCHEMA = automation.maybe_simple_id( + cv.Schema( + { + cv.GenerateID(): cv.use_id(ESP32BLETracker), + } + ) +) + + +@automation.register_action( + "esp32_ble_tracker.stop_scan", + ESP32BLEStopScanAction, + ESP32_BLE_STOP_SCAN_ACTION_SCHEMA, +) +async def esp32_ble_tracker_stop_scan_action_to_code( + config, action_id, template_arg, args +): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var async def register_ble_device(var, config): diff --git a/esphome/components/esp32_ble_tracker/automation.h b/esphome/components/esp32_ble_tracker/automation.h index 3505e9c26d..6bef9edcb3 100644 --- a/esphome/components/esp32_ble_tracker/automation.h +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -10,18 +10,22 @@ namespace esp32_ble_tracker { class ESPBTAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { public: explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } - void set_address(uint64_t address) { this->address_ = address; } + void set_addresses(const std::vector &addresses) { this->address_vec_ = addresses; } bool parse_device(const ESPBTDevice &device) override { - if (this->address_ && device.address_uint64() != this->address_) { - return false; + uint64_t u64_addr = device.address_uint64(); + if (!address_vec_.empty()) { + if (std::find(address_vec_.begin(), address_vec_.end(), u64_addr) == address_vec_.end()) { + return false; + } } + this->trigger(device); return true; } protected: - uint64_t address_ = 0; + std::vector address_vec_; }; class BLEServiceDataAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { @@ -76,6 +80,32 @@ class BLEManufacturerDataAdvertiseTrigger : public Trigger, ESPBTUUID uuid_; }; +class BLEEndOfScanTrigger : public Trigger<>, public ESPBTDeviceListener { + public: + explicit BLEEndOfScanTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } + + bool parse_device(const ESPBTDevice &device) override { return false; } + void on_scan_end() override { this->trigger(); } +}; + +template class ESP32BLEStartScanAction : public Action { + public: + ESP32BLEStartScanAction(ESP32BLETracker *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(bool, continuous) + void play(Ts... x) override { + this->parent_->set_scan_continuous(this->continuous_.value(x...)); + this->parent_->start_scan(); + } + + protected: + ESP32BLETracker *parent_; +}; + +template class ESP32BLEStopScanAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->stop_scan(); } +}; + } // namespace esp32_ble_tracker } // namespace esphome diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 7614e33979..0f6c4117d2 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -1,19 +1,24 @@ #ifdef USE_ESP32 #include "esp32_ble_tracker.h" -#include "esphome/core/log.h" #include "esphome/core/application.h" -#include "esphome/core/helpers.h" +#include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" -#include -#include -#include #include -#include -#include -#include #include +#include +#include +#include +#include +#include +#include + +#ifdef USE_OTA +#include "esphome/components/ota/ota_component.h" +#endif #ifdef USE_ARDUINO #include @@ -29,188 +34,211 @@ static const char *const TAG = "esp32_ble_tracker"; ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { - uint64_t u = 0; - u |= uint64_t(address[0] & 0xFF) << 40; - u |= uint64_t(address[1] & 0xFF) << 32; - u |= uint64_t(address[2] & 0xFF) << 24; - u |= uint64_t(address[3] & 0xFF) << 16; - u |= uint64_t(address[4] & 0xFF) << 8; - u |= uint64_t(address[5] & 0xFF) << 0; - return u; -} - -float ESP32BLETracker::get_setup_priority() const { return setup_priority::BLUETOOTH; } +float ESP32BLETracker::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } void ESP32BLETracker::setup() { + if (this->parent_->is_failed()) { + this->mark_failed(); + ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE"); + return; + } + ExternalRAMAllocator allocator( + ExternalRAMAllocator::ALLOW_FAILURE); + this->scan_result_buffer_ = allocator.allocate(ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE); + + if (this->scan_result_buffer_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate buffer for BLE Tracker!"); + this->mark_failed(); + } + global_esp32_ble_tracker = this; this->scan_result_lock_ = xSemaphoreCreateMutex(); this->scan_end_lock_ = xSemaphoreCreateMutex(); + this->scanner_idle_ = true; - if (!ESP32BLETracker::ble_setup()) { - this->mark_failed(); - return; +#ifdef USE_OTA + ota::global_ota_component->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t error) { + if (state == ota::OTA_STARTED) { + this->stop_scan(); + } + }); +#endif + + if (this->scan_continuous_) { + if (xSemaphoreTake(this->scan_end_lock_, 0L)) { + this->start_scan_(true); + } else { + ESP_LOGW(TAG, "Cannot start scan!"); + } } - - global_esp32_ble_tracker->start_scan_(true); } void ESP32BLETracker::loop() { - BLEEvent *ble_event = this->ble_events_.pop(); - while (ble_event != nullptr) { - if (ble_event->type_) { - this->real_gattc_event_handler_(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if, - &ble_event->event_.gattc.gattc_param); - } else { - this->real_gap_event_handler_(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param); - } - delete ble_event; // NOLINT(cppcoreguidelines-owning-memory) - ble_event = this->ble_events_.pop(); - } - - bool connecting = false; + int connecting = 0; + int discovered = 0; + int searching = 0; + int disconnecting = 0; for (auto *client : this->clients_) { - if (client->state() == ClientState::CONNECTING || client->state() == ClientState::DISCOVERED) - connecting = true; - } - if (!connecting && xSemaphoreTake(this->scan_end_lock_, 0L)) { - xSemaphoreGive(this->scan_end_lock_); - global_esp32_ble_tracker->start_scan_(false); - } - - if (xSemaphoreTake(this->scan_result_lock_, 5L / portTICK_PERIOD_MS)) { - uint32_t index = this->scan_result_index_; - xSemaphoreGive(this->scan_result_lock_); - - if (index >= 16) { - ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); + switch (client->state()) { + case ClientState::DISCONNECTING: + disconnecting++; + break; + case ClientState::DISCOVERED: + discovered++; + break; + case ClientState::SEARCHING: + searching++; + break; + case ClientState::CONNECTING: + case ClientState::READY_TO_CONNECT: + connecting++; + break; + default: + break; } - for (size_t i = 0; i < index; i++) { - ESPBTDevice device; - device.parse_scan_rst(this->scan_result_buffer_[i]); + } + bool promote_to_connecting = discovered && !searching && !connecting; - bool found = false; - for (auto *listener : this->listeners_) { - if (listener->parse_device(device)) - found = true; + if (!this->scanner_idle_) { + if (this->scan_result_index_ && // if it looks like we have a scan result we will take the lock + xSemaphoreTake(this->scan_result_lock_, 5L / portTICK_PERIOD_MS)) { + uint32_t index = this->scan_result_index_; + if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { + ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); } + bool bulk_parsed = false; + + for (auto *listener : this->listeners_) { + bulk_parsed |= listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_); + } for (auto *client : this->clients_) { - if (client->parse_device(device)) { - found = true; - if (client->state() == ClientState::DISCOVERED) { - esp_ble_gap_stop_scanning(); - if (xSemaphoreTake(this->scan_end_lock_, 10L / portTICK_PERIOD_MS)) { - xSemaphoreGive(this->scan_end_lock_); + bulk_parsed |= client->parse_devices(this->scan_result_buffer_, this->scan_result_index_); + } + + if (!bulk_parsed) { + for (size_t i = 0; i < index; i++) { + ESPBTDevice device; + device.parse_scan_rst(this->scan_result_buffer_[i]); + + bool found = false; + for (auto *listener : this->listeners_) { + if (listener->parse_device(device)) + found = true; + } + + for (auto *client : this->clients_) { + if (client->parse_device(device)) { + found = true; + if (!connecting && client->state() == ClientState::DISCOVERED) { + promote_to_connecting = true; + } } } + + if (!found && !this->scan_continuous_) { + this->print_bt_device_info(device); + } } } - - if (!found) { - this->print_bt_device_info(device); - } - } - - if (xSemaphoreTake(this->scan_result_lock_, 10L / portTICK_PERIOD_MS)) { this->scan_result_index_ = 0; xSemaphoreGive(this->scan_result_lock_); } + + /* + + Avoid starting the scanner if: + - we are already scanning + - we are connecting to a device + - we are disconnecting from a device + + Otherwise the scanner could fail to ever start again + and our only way to recover is to reboot. + + https://github.com/espressif/esp-idf/issues/6688 + + */ + if (!connecting && !disconnecting && xSemaphoreTake(this->scan_end_lock_, 0L)) { + if (this->scan_continuous_) { + if (!promote_to_connecting && !this->scan_start_failed_ && !this->scan_set_param_failed_) { + this->start_scan_(false); + } else { + // We didn't start the scan, so we need to release the lock + xSemaphoreGive(this->scan_end_lock_); + } + } else if (!this->scanner_idle_) { + this->end_of_scan_(); + return; + } + } + + if (this->scan_start_failed_ || this->scan_set_param_failed_) { + if (this->scan_start_fail_count_ == 255) { + ESP_LOGE(TAG, "ESP-IDF BLE scan could not restart after 255 attempts, rebooting to restore BLE stack..."); + App.reboot(); + } + if (xSemaphoreTake(this->scan_end_lock_, 0L)) { + xSemaphoreGive(this->scan_end_lock_); + } else { + ESP_LOGD(TAG, "Stopping scan after failure..."); + esp_ble_gap_stop_scanning(); + this->cancel_timeout("scan"); + } + if (this->scan_start_failed_) { + ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_); + this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS; + } + if (this->scan_set_param_failed_) { + ESP_LOGE(TAG, "Scan set param failed: %d", this->scan_set_param_failed_); + this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS; + } + } } - if (this->scan_set_param_failed_) { - ESP_LOGE(TAG, "Scan set param failed: %d", this->scan_set_param_failed_); - this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS; - } - - if (this->scan_start_failed_) { - ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_); - this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS; + // If there is a discovered client and no connecting + // clients and no clients using the scanner to search for + // devices, then stop scanning and promote the discovered + // client to ready to connect. + if (promote_to_connecting) { + for (auto *client : this->clients_) { + if (client->state() == ClientState::DISCOVERED) { + if (xSemaphoreTake(this->scan_end_lock_, 0L)) { + // Scanner is not running since we got the + // lock, so we can promote the client. + xSemaphoreGive(this->scan_end_lock_); + // We only want to promote one client at a time. + // once the scanner is fully stopped. + client->set_state(ClientState::READY_TO_CONNECT); + } else { + ESP_LOGD(TAG, "Pausing scan to make connection..."); + esp_ble_gap_stop_scanning(); + this->cancel_timeout("scan"); + } + break; + } + } } } -bool ESP32BLETracker::ble_setup() { - // Initialize non-volatile storage for the bluetooth controller - esp_err_t err = nvs_flash_init(); - if (err != ESP_OK) { - ESP_LOGE(TAG, "nvs_flash_init failed: %d", err); - return false; +void ESP32BLETracker::start_scan() { + if (xSemaphoreTake(this->scan_end_lock_, 0L)) { + this->start_scan_(true); + } else { + ESP_LOGW(TAG, "Scan requested when a scan is already in progress. Ignoring."); } +} -#ifdef USE_ARDUINO - if (!btStart()) { - ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status()); - return false; - } -#else - if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { - // start bt controller - if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { - esp_bt_controller_config_t cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); - err = esp_bt_controller_init(&cfg); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bt_controller_init failed: %s", esp_err_to_name(err)); - return false; - } - while (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) - ; - } - if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_INITED) { - err = esp_bt_controller_enable(ESP_BT_MODE_BLE); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bt_controller_enable failed: %s", esp_err_to_name(err)); - return false; - } - } - if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { - ESP_LOGE(TAG, "esp bt controller enable failed"); - return false; - } - } -#endif - - esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); - - err = esp_bluedroid_init(); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bluedroid_init failed: %d", err); - return false; - } - err = esp_bluedroid_enable(); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err); - return false; - } - err = esp_ble_gap_register_callback(ESP32BLETracker::gap_event_handler); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); - return false; - } - err = esp_ble_gattc_register_callback(ESP32BLETracker::gattc_event_handler); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err); - return false; - } - - // Empty name - esp_ble_gap_set_device_name(""); - - esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE; - err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t)); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gap_set_security_param failed: %d", err); - return false; - } - - // BLE takes some time to be fully set up, 200ms should be more than enough - delay(200); // NOLINT - - return true; +void ESP32BLETracker::stop_scan() { + ESP_LOGD(TAG, "Stopping scan."); + this->scan_continuous_ = false; + esp_ble_gap_stop_scanning(); + this->cancel_timeout("scan"); } void ESP32BLETracker::start_scan_(bool first) { - if (!xSemaphoreTake(this->scan_end_lock_, 0L)) { - ESP_LOGW(TAG, "Cannot start scan!"); + // The lock must be held when calling this function. + if (xSemaphoreTake(this->scan_end_lock_, 0L)) { + ESP_LOGE(TAG, "start_scan called without holding scan_end_lock_"); return; } @@ -220,6 +248,7 @@ void ESP32BLETracker::start_scan_(bool first) { listener->on_scan_end(); } this->already_discovered_.clear(); + this->scanner_idle_ = false; this->scan_params_.scan_type = this->scan_active_ ? BLE_SCAN_TYPE_ACTIVE : BLE_SCAN_TYPE_PASSIVE; this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC; this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL; @@ -230,38 +259,53 @@ void ESP32BLETracker::start_scan_(bool first) { esp_ble_gap_start_scanning(this->scan_duration_); this->set_timeout("scan", this->scan_duration_ * 2000, []() { - ESP_LOGW(TAG, "ESP-IDF BLE scan never terminated, rebooting to restore BLE stack..."); + ESP_LOGE(TAG, "ESP-IDF BLE scan never terminated, rebooting to restore BLE stack..."); App.reboot(); }); } +void ESP32BLETracker::end_of_scan_() { + // The lock must be held when calling this function. + if (xSemaphoreTake(this->scan_end_lock_, 0L)) { + ESP_LOGE(TAG, "end_of_scan_ called without holding the scan_end_lock_"); + return; + } + + ESP_LOGD(TAG, "End of scan."); + this->scanner_idle_ = true; + this->already_discovered_.clear(); + xSemaphoreGive(this->scan_end_lock_); + this->cancel_timeout("scan"); + + for (auto *listener : this->listeners_) + listener->on_scan_end(); +} + void ESP32BLETracker::register_client(ESPBTClient *client) { client->app_id = ++this->app_id_; this->clients_.push_back(client); } void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { - BLEEvent *gap_event = new BLEEvent(event, param); // NOLINT(cppcoreguidelines-owning-memory) - global_esp32_ble_tracker->ble_events_.push(gap_event); -} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) - -void ESP32BLETracker::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { case ESP_GAP_BLE_SCAN_RESULT_EVT: - global_esp32_ble_tracker->gap_scan_result_(param->scan_rst); + this->gap_scan_result_(param->scan_rst); break; case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: - global_esp32_ble_tracker->gap_scan_set_param_complete_(param->scan_param_cmpl); + this->gap_scan_set_param_complete_(param->scan_param_cmpl); break; case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: - global_esp32_ble_tracker->gap_scan_start_complete_(param->scan_start_cmpl); + this->gap_scan_start_complete_(param->scan_start_cmpl); break; case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: - global_esp32_ble_tracker->gap_scan_stop_complete_(param->scan_stop_cmpl); + this->gap_scan_stop_complete_(param->scan_stop_cmpl); break; default: break; } + for (auto *client : this->clients_) { + client->gap_event_handler(event, param); + } } void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m) { @@ -270,6 +314,12 @@ void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t: void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m) { this->scan_start_failed_ = param.status; + if (param.status == ESP_BT_STATUS_SUCCESS) { + this->scan_start_fail_count_ = 0; + } else { + this->scan_start_fail_count_++; + xSemaphoreGive(this->scan_end_lock_); + } } void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m) { @@ -279,7 +329,7 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_ void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { if (xSemaphoreTake(this->scan_result_lock_, 0L)) { - if (this->scan_result_index_ < 16) { + if (this->scan_result_index_ < ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { this->scan_result_buffer_[this->scan_result_index_++] = param; } xSemaphoreGive(this->scan_result_lock_); @@ -291,190 +341,11 @@ void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_re void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { - BLEEvent *gattc_event = new BLEEvent(event, gattc_if, param); // NOLINT(cppcoreguidelines-owning-memory) - global_esp32_ble_tracker->ble_events_.push(gattc_event); -} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) - -void ESP32BLETracker::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) { - for (auto *client : global_esp32_ble_tracker->clients_) { + for (auto *client : this->clients_) { client->gattc_event_handler(event, gattc_if, param); } } -ESPBTUUID::ESPBTUUID() : uuid_() {} -ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) { - ESPBTUUID ret; - ret.uuid_.len = ESP_UUID_LEN_16; - ret.uuid_.uuid.uuid16 = uuid; - return ret; -} -ESPBTUUID ESPBTUUID::from_uint32(uint32_t uuid) { - ESPBTUUID ret; - ret.uuid_.len = ESP_UUID_LEN_32; - ret.uuid_.uuid.uuid32 = uuid; - return ret; -} -ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) { - ESPBTUUID ret; - ret.uuid_.len = ESP_UUID_LEN_128; - for (size_t i = 0; i < ESP_UUID_LEN_128; i++) - ret.uuid_.uuid.uuid128[i] = data[i]; - return ret; -} -ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { - ESPBTUUID ret; - if (data.length() == 4) { - ret.uuid_.len = ESP_UUID_LEN_16; - ret.uuid_.uuid.uuid16 = 0; - for (int i = 0; i < data.length();) { - uint8_t msb = data.c_str()[i]; - uint8_t lsb = data.c_str()[i + 1]; - - if (msb > '9') - msb -= 7; - if (lsb > '9') - lsb -= 7; - ret.uuid_.uuid.uuid16 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << (2 - i) * 4; - i += 2; - } - } else if (data.length() == 8) { - ret.uuid_.len = ESP_UUID_LEN_32; - ret.uuid_.uuid.uuid32 = 0; - for (int i = 0; i < data.length();) { - uint8_t msb = data.c_str()[i]; - uint8_t lsb = data.c_str()[i + 1]; - - if (msb > '9') - msb -= 7; - if (lsb > '9') - lsb -= 7; - ret.uuid_.uuid.uuid32 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << (6 - i) * 4; - i += 2; - } - } else if (data.length() == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be - // investigated (lack of time) - ret.uuid_.len = ESP_UUID_LEN_128; - memcpy(ret.uuid_.uuid.uuid128, (uint8_t *) data.data(), 16); - } else if (data.length() == 36) { - // If the length of the string is 36 bytes then we will assume it is a long hex string in - // UUID format. - ret.uuid_.len = ESP_UUID_LEN_128; - int n = 0; - for (int i = 0; i < data.length();) { - if (data.c_str()[i] == '-') - i++; - uint8_t msb = data.c_str()[i]; - uint8_t lsb = data.c_str()[i + 1]; - - if (msb > '9') - msb -= 7; - if (lsb > '9') - lsb -= 7; - ret.uuid_.uuid.uuid128[15 - n++] = ((msb & 0x0F) << 4) | (lsb & 0x0F); - i += 2; - } - } else { - ESP_LOGE(TAG, "ERROR: UUID value not 2, 4, 16 or 36 bytes - %s", data.c_str()); - } - return ret; -} -ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) { - ESPBTUUID ret; - ret.uuid_.len = uuid.len; - ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16; - ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32; - for (size_t i = 0; i < ESP_UUID_LEN_128; i++) - ret.uuid_.uuid.uuid128[i] = uuid.uuid.uuid128[i]; - return ret; -} -ESPBTUUID ESPBTUUID::as_128bit() const { - if (this->uuid_.len == ESP_UUID_LEN_128) { - return *this; - } - uint8_t data[] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - uint32_t uuid32; - if (this->uuid_.len == ESP_UUID_LEN_32) { - uuid32 = this->uuid_.uuid.uuid32; - } else { - uuid32 = this->uuid_.uuid.uuid16; - } - for (uint8_t i = 0; i < this->uuid_.len; i++) { - data[12 + i] = ((uuid32 >> i * 8) & 0xFF); - } - return ESPBTUUID::from_raw(data); -} -bool ESPBTUUID::contains(uint8_t data1, uint8_t data2) const { - if (this->uuid_.len == ESP_UUID_LEN_16) { - return (this->uuid_.uuid.uuid16 >> 8) == data2 && (this->uuid_.uuid.uuid16 & 0xFF) == data1; - } else if (this->uuid_.len == ESP_UUID_LEN_32) { - for (uint8_t i = 0; i < 3; i++) { - bool a = ((this->uuid_.uuid.uuid32 >> i * 8) & 0xFF) == data1; - bool b = ((this->uuid_.uuid.uuid32 >> (i + 1) * 8) & 0xFF) == data2; - if (a && b) - return true; - } - } else { - for (uint8_t i = 0; i < 15; i++) { - if (this->uuid_.uuid.uuid128[i] == data1 && this->uuid_.uuid.uuid128[i + 1] == data2) - return true; - } - } - return false; -} -bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const { - if (this->uuid_.len == uuid.uuid_.len) { - switch (this->uuid_.len) { - case ESP_UUID_LEN_16: - if (uuid.uuid_.uuid.uuid16 == this->uuid_.uuid.uuid16) { - return true; - } - break; - case ESP_UUID_LEN_32: - if (uuid.uuid_.uuid.uuid32 == this->uuid_.uuid.uuid32) { - return true; - } - break; - case ESP_UUID_LEN_128: - for (int i = 0; i < ESP_UUID_LEN_128; i++) { - if (uuid.uuid_.uuid.uuid128[i] != this->uuid_.uuid.uuid128[i]) { - return false; - } - } - return true; - break; - } - } else { - return this->as_128bit() == uuid.as_128bit(); - } - return false; -} -esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; } -std::string ESPBTUUID::to_string() const { - char sbuf[64]; - switch (this->uuid_.len) { - case ESP_UUID_LEN_16: - sprintf(sbuf, "0x%02X%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); - break; - case ESP_UUID_LEN_32: - sprintf(sbuf, "0x%02X%02X%02X%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff), - (this->uuid_.uuid.uuid32 >> 8 & 0xff), this->uuid_.uuid.uuid32 & 0xff); - break; - default: - case ESP_UUID_LEN_128: - char *bpos = sbuf; - for (int8_t i = 15; i >= 0; i--) { - sprintf(bpos, "%02X", this->uuid_.uuid.uuid128[i]); - bpos += 2; - if (i == 6 || i == 8 || i == 10 || i == 12) - sprintf(bpos++, "-"); - } - sbuf[47] = '\0'; - break; - } - return sbuf; -} - ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(beacon_data_)); } optional ESPBLEiBeacon::from_manufacturer_data(const ServiceData &data) { if (!data.uuid.contains(0x4C, 0x00)) @@ -554,8 +425,9 @@ void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_p while (offset + 2 < len) { const uint8_t field_length = payload[offset++]; // First byte is length of adv record - if (field_length == 0) - break; + if (field_length == 0) { + continue; // Possible zero padded advertisement data + } // first byte of adv record is adv record type const uint8_t record_type = payload[offset++]; @@ -570,11 +442,17 @@ void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_p // (called CSS here) switch (record_type) { + case ESP_BLE_AD_TYPE_NAME_SHORT: case ESP_BLE_AD_TYPE_NAME_CMPL: { // CSS 1.2 LOCAL NAME // "The Local Name data type shall be the same as, or a shortened version of, the local name assigned to the // device." CSS 1: Optional in this context; shall not appear more than once in a block. - this->name_ = std::string(reinterpret_cast(record), record_length); + // SHORTENED LOCAL NAME + // "The Shortened Local Name data type defines a shortened version of the Local Name data type. The Shortened + // Local Name data type shall not be used to advertise a name that is longer than the Local Name data type." + if (record_length > this->name_.length()) { + this->name_ = std::string(reinterpret_cast(record), record_length); + } break; } case ESP_BLE_AD_TYPE_TX_PWR: { @@ -690,6 +568,9 @@ void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_p this->service_datas_.push_back(data); break; } + case ESP_BLE_AD_TYPE_INT_RANGE: + // Avoid logging this as it's very verbose + break; default: { ESP_LOGV(TAG, "Unhandled type: advType: 0x%02x", record_type); break; @@ -703,7 +584,7 @@ std::string ESPBTDevice::address_str() const { this->address_[3], this->address_[4], this->address_[5]); return mac; } -uint64_t ESPBTDevice::address_uint64() const { return ble_addr_to_uint64(this->address_); } +uint64_t ESPBTDevice::address_uint64() const { return esp32_ble::ble_addr_to_uint64(this->address_); } void ESP32BLETracker::dump_config() { ESP_LOGCONFIG(TAG, "BLE Tracker:"); @@ -711,7 +592,9 @@ void ESP32BLETracker::dump_config() { ESP_LOGCONFIG(TAG, " Scan Interval: %.1f ms", this->scan_interval_ * 0.625f); ESP_LOGCONFIG(TAG, " Scan Window: %.1f ms", this->scan_window_ * 0.625f); ESP_LOGCONFIG(TAG, " Scan Type: %s", this->scan_active_ ? "ACTIVE" : "PASSIVE"); + ESP_LOGCONFIG(TAG, " Continuous Scanning: %s", this->scan_continuous_ ? "True" : "False"); } + void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) { const uint64_t address = device.address_uint64(); for (auto &disc : this->already_discovered_) { @@ -742,8 +625,9 @@ void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) { } ESP_LOGD(TAG, " Address Type: %s", address_type_s); - if (!device.get_name().empty()) + if (!device.get_name().empty()) { ESP_LOGD(TAG, " Name: '%s'", device.get_name().c_str()); + } for (auto &tx_power : device.get_tx_powers()) { ESP_LOGD(TAG, " TX Power: %d", tx_power); } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 9ff2a5a861..43e88fbf2b 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -1,48 +1,29 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" -#include "queue.h" + +#include +#include +#include #ifdef USE_ESP32 -#include -#include #include #include #include +#include +#include + +#include "esphome/components/esp32_ble/ble.h" +#include "esphome/components/esp32_ble/ble_uuid.h" + namespace esphome { namespace esp32_ble_tracker { -class ESPBTUUID { - public: - ESPBTUUID(); - - static ESPBTUUID from_uint16(uint16_t uuid); - - static ESPBTUUID from_uint32(uint32_t uuid); - - static ESPBTUUID from_raw(const uint8_t *data); - - static ESPBTUUID from_raw(const std::string &data); - - static ESPBTUUID from_uuid(esp_bt_uuid_t uuid); - - ESPBTUUID as_128bit() const; - - bool contains(uint8_t data1, uint8_t data2) const; - - bool operator==(const ESPBTUUID &uuid) const; - bool operator!=(const ESPBTUUID &uuid) const { return !(*this == uuid); } - - esp_bt_uuid_t get_uuid() const; - - std::string to_string() const; - - protected: - esp_bt_uuid_t uuid_; -}; +using namespace esp32_ble; using adv_data_t = std::vector; @@ -120,7 +101,7 @@ class ESPBTDevice { std::vector tx_powers_{}; optional appearance_{}; optional ad_flag_{}; - std::vector service_uuids_; + std::vector service_uuids_{}; std::vector manufacturer_datas_{}; std::vector service_datas_{}; esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_{}; @@ -132,6 +113,9 @@ class ESPBTDeviceListener { public: virtual void on_scan_end() {} virtual bool parse_device(const ESPBTDevice &device) = 0; + virtual bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { + return false; + }; void set_parent(ESP32BLETracker *parent) { parent_ = parent; } protected: @@ -139,10 +123,18 @@ class ESPBTDeviceListener { }; enum class ClientState { + // Connection is allocated + INIT, + // Client is disconnecting + DISCONNECTING, // Connection is idle, no device detected. IDLE, + // Searching for device. + SEARCHING, // Device advertisement found. DISCOVERED, + // Device is discovered and the scanner is stopped + READY_TO_CONNECT, // Connection in progress. CONNECTING, // Initial connection established. @@ -151,12 +143,25 @@ enum class ClientState { ESTABLISHED, }; +enum class ConnectionType { + // The default connection type, we hold all the services in ram + // for the duration of the connection. + V1, + // The client has a cache of the services and mtu so we should not + // fetch them again + V3_WITH_CACHE, + // The client does not need the services and mtu once we send them + // so we should wipe them from memory as soon as we send them + V3_WITHOUT_CACHE +}; + class ESPBTClient : public ESPBTDeviceListener { public: - virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + virtual bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) = 0; + virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; virtual void connect() = 0; - void set_state(ClientState st) { this->state_ = st; } + virtual void set_state(ClientState st) { this->state_ = st; } ClientState state() const { return state_; } int app_id; @@ -164,12 +169,13 @@ class ESPBTClient : public ESPBTDeviceListener { ClientState state_; }; -class ESP32BLETracker : public Component { +class ESP32BLETracker : public Component, public GAPEventHandler, public GATTcEventHandler, public Parented { public: void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; } void set_scan_interval(uint32_t scan_interval) { scan_interval_ = scan_interval; } void set_scan_window(uint32_t scan_window) { scan_window_ = scan_window; } void set_scan_active(bool scan_active) { scan_active_ = scan_active; } + void set_scan_continuous(bool scan_continuous) { scan_continuous_ = scan_continuous; } /// Setup the FreeRTOS task and the Bluetooth stack. void setup() override; @@ -187,14 +193,18 @@ class ESP32BLETracker : public Component { void print_bt_device_info(const ESPBTDevice &device); + void start_scan(); + void stop_scan(); + + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; + protected: - /// The FreeRTOS task managing the bluetooth interface. - static bool ble_setup(); /// Start a single scan by setting up the parameters and doing some esp-idf calls. void start_scan_(bool first); - /// Callback that will handle all GAP events and redistribute them to other callbacks. - static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); - void real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + /// Called when a scan ends + void end_of_scan_(); /// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received. void gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); /// Called when a `ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT` event is received. @@ -205,9 +215,6 @@ class ESP32BLETracker : public Component { void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m); int app_id_; - /// Callback that will handle all GATTC events and redistribute them to other callbacks. - static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); - void real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); /// Vector of addresses that have already been printed in print_bt_device_info std::vector already_discovered_; @@ -220,15 +227,21 @@ class ESP32BLETracker : public Component { uint32_t scan_duration_; uint32_t scan_interval_; uint32_t scan_window_; + uint8_t scan_start_fail_count_; + bool scan_continuous_; bool scan_active_; + bool scanner_idle_; SemaphoreHandle_t scan_result_lock_; SemaphoreHandle_t scan_end_lock_; size_t scan_result_index_{0}; - esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_buffer_[16]; +#if CONFIG_SPIRAM + const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 32; +#else + const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 16; +#endif // CONFIG_SPIRAM + esp_ble_gap_cb_param_t::ble_scan_result_evt_param *scan_result_buffer_; esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; - - Queue ble_events_; }; // NOLINTNEXTLINE diff --git a/esphome/components/esp32_ble_tracker/queue.h b/esphome/components/esp32_ble_tracker/queue.h deleted file mode 100644 index f09b2ca8d7..0000000000 --- a/esphome/components/esp32_ble_tracker/queue.h +++ /dev/null @@ -1,108 +0,0 @@ -#pragma once - -#ifdef USE_ESP32 -#include "esphome/core/component.h" -#include "esphome/core/helpers.h" - -#include -#include -#include - -#include -#include -#include -#include - -/* - * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather - * than trying to deal with various locking strategies, all incoming GAP and GATT - * events will simply be placed on a semaphore guarded queue. The next time the - * component runs loop(), these events are popped off the queue and handed at - * this safer time. - */ - -namespace esphome { -namespace esp32_ble_tracker { - -template class Queue { - public: - Queue() { m_ = xSemaphoreCreateMutex(); } - - void push(T *element) { - if (element == nullptr) - return; - if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) { - q_.push(element); - xSemaphoreGive(m_); - } - } - - T *pop() { - T *element = nullptr; - - if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) { - if (!q_.empty()) { - element = q_.front(); - q_.pop(); - } - xSemaphoreGive(m_); - } - return element; - } - - protected: - std::queue q_; - SemaphoreHandle_t m_; -}; - -// Received GAP and GATTC events are only queued, and get processed in the main loop(). -// This class stores each event in a single type. -class BLEEvent { - public: - BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { - this->event_.gap.gap_event = e; - memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t)); - this->type_ = 0; - }; - - BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { - this->event_.gattc.gattc_event = e; - this->event_.gattc.gattc_if = i; - memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t)); - // Need to also make a copy of relevant event data. - switch (e) { - case ESP_GATTC_NOTIFY_EVT: - memcpy(this->event_.gattc.data, p->notify.value, p->notify.value_len); - this->event_.gattc.gattc_param.notify.value = this->event_.gattc.data; - break; - case ESP_GATTC_READ_CHAR_EVT: - case ESP_GATTC_READ_DESCR_EVT: - memcpy(this->event_.gattc.data, p->read.value, p->read.value_len); - this->event_.gattc.gattc_param.read.value = this->event_.gattc.data; - break; - default: - break; - } - this->type_ = 1; - }; - - union { - struct gap_event { // NOLINT(readability-identifier-naming) - esp_gap_ble_cb_event_t gap_event; - esp_ble_gap_cb_param_t gap_param; - } gap; - - struct gattc_event { // NOLINT(readability-identifier-naming) - esp_gattc_cb_event_t gattc_event; - esp_gatt_if_t gattc_if; - esp_ble_gattc_cb_param_t gattc_param; - uint8_t data[64]; - } gattc; - } event_; - uint8_t type_; // 0=gap 1=gattc -}; - -} // namespace esp32_ble_tracker -} // namespace esphome - -#endif diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 912e705766..4cbdf7ca5c 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome import automation from esphome import pins from esphome.const import ( CONF_FREQUENCY, @@ -12,6 +13,7 @@ from esphome.const import ( CONF_RESOLUTION, CONF_BRIGHTNESS, CONF_CONTRAST, + CONF_TRIGGER_ID, ) from esphome.core import CORE from esphome.components.esp32 import add_idf_sdkconfig_option @@ -23,7 +25,14 @@ AUTO_LOAD = ["psram"] esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) - +ESP32CameraStreamStartTrigger = esp32_camera_ns.class_( + "ESP32CameraStreamStartTrigger", + automation.Trigger.template(), +) +ESP32CameraStreamStopTrigger = esp32_camera_ns.class_( + "ESP32CameraStreamStopTrigger", + automation.Trigger.template(), +) ESP32CameraFrameSize = esp32_camera_ns.enum("ESP32CameraFrameSize") FRAME_SIZES = { "160X120": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120, @@ -46,6 +55,22 @@ FRAME_SIZES = { "SXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1280X1024, "1600X1200": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1600X1200, "UXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1600X1200, + "1920X1080": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1920X1080, + "FHD": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1920X1080, + "720X1280": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_720X1280, + "PHD": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_720X1280, + "864X1536": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_864X1536, + "P3MP": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_864X1536, + "2048X1536": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2048X1536, + "QXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2048X1536, + "2560X1440": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1440, + "QHD": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1440, + "2560X1600": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1600, + "WQXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1600, + "1080X1920": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1080X1920, + "PFHD": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1080X1920, + "2560X1920": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1920, + "QSXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1920, } ESP32GainControlMode = esp32_camera_ns.enum("ESP32GainControlMode") ENUM_GAIN_CONTROL_MODE = { @@ -111,6 +136,10 @@ CONF_TEST_PATTERN = "test_pattern" CONF_MAX_FRAMERATE = "max_framerate" CONF_IDLE_FRAMERATE = "idle_framerate" +# stream trigger +CONF_ON_STREAM_START = "on_stream_start" +CONF_ON_STREAM_STOP = "on_stream_stop" + camera_range_param = cv.int_range(min=-2, max=2) CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( @@ -127,7 +156,7 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( { cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number, cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All( - cv.frequency, cv.one_of(20e6, 10e6) + cv.frequency, cv.Range(min=8e6, max=20e6) ), } ), @@ -143,7 +172,7 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum( FRAME_SIZES, upper=True ), - cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=10, max=63), + cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63), cv.Optional(CONF_CONTRAST, default=0): camera_range_param, cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param, cv.Optional(CONF_SATURATION, default=0): camera_range_param, @@ -178,6 +207,20 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All( cv.framerate, cv.Range(min=0, max=1) ), + cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32CameraStreamStartTrigger + ), + } + ), + cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32CameraStreamStopTrigger + ), + } + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -238,3 +281,11 @@ async def to_code(config): if CORE.using_esp_idf: cg.add_library("espressif/esp32-camera", "1.0.0") add_idf_sdkconfig_option("CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC", True) + + for conf in config.get(CONF_ON_STREAM_START, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_STREAM_STOP, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 851926b083..e4020a902e 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -54,7 +54,11 @@ void ESP32Camera::dump_config() { ESP_LOGCONFIG(TAG, " HREF Pin: %d", conf.pin_href); ESP_LOGCONFIG(TAG, " Pixel Clock Pin: %d", conf.pin_pclk); ESP_LOGCONFIG(TAG, " External Clock: Pin:%d Frequency:%u", conf.pin_xclk, conf.xclk_freq_hz); +#ifdef USE_ESP_IDF // Temporary until the espressif/esp32-camera library is updated ESP_LOGCONFIG(TAG, " I2C Pins: SDA:%d SCL:%d", conf.pin_sscb_sda, conf.pin_sscb_scl); +#else + ESP_LOGCONFIG(TAG, " I2C Pins: SDA:%d SCL:%d", conf.pin_sccb_sda, conf.pin_sccb_scl); +#endif ESP_LOGCONFIG(TAG, " Reset Pin: %d", conf.pin_reset); switch (this->config_.frame_size) { case FRAMESIZE_QQVGA: @@ -87,6 +91,30 @@ void ESP32Camera::dump_config() { case FRAMESIZE_UXGA: ESP_LOGCONFIG(TAG, " Resolution: 1600x1200 (UXGA)"); break; + case FRAMESIZE_FHD: + ESP_LOGCONFIG(TAG, " Resolution: 1920x1080 (FHD)"); + break; + case FRAMESIZE_P_HD: + ESP_LOGCONFIG(TAG, " Resolution: 720x1280 (P_HD)"); + break; + case FRAMESIZE_P_3MP: + ESP_LOGCONFIG(TAG, " Resolution: 864x1536 (P_3MP)"); + break; + case FRAMESIZE_QXGA: + ESP_LOGCONFIG(TAG, " Resolution: 2048x1536 (QXGA)"); + break; + case FRAMESIZE_QHD: + ESP_LOGCONFIG(TAG, " Resolution: 2560x1440 (QHD)"); + break; + case FRAMESIZE_WQXGA: + ESP_LOGCONFIG(TAG, " Resolution: 2560x1600 (WQXGA)"); + break; + case FRAMESIZE_P_FHD: + ESP_LOGCONFIG(TAG, " Resolution: 1080x1920 (P_FHD)"); + break; + case FRAMESIZE_QSXGA: + ESP_LOGCONFIG(TAG, " Resolution: 2560x1920 (QSXGA)"); + break; default: break; } @@ -174,7 +202,7 @@ void ESP32Camera::loop() { float ESP32Camera::get_setup_priority() const { return setup_priority::DATA; } /* ---------------- constructors ---------------- */ -ESP32Camera::ESP32Camera(const std::string &name) : EntityBase(name) { +ESP32Camera::ESP32Camera() { this->config_.pin_pwdn = -1; this->config_.pin_reset = -1; this->config_.pin_xclk = -1; @@ -187,7 +215,6 @@ ESP32Camera::ESP32Camera(const std::string &name) : EntityBase(name) { global_esp32_camera = this; } -ESP32Camera::ESP32Camera() : ESP32Camera("") {} /* ---------------- setters ---------------- */ /* set pin assignment */ @@ -209,8 +236,13 @@ void ESP32Camera::set_external_clock(uint8_t pin, uint32_t frequency) { this->config_.xclk_freq_hz = frequency; } void ESP32Camera::set_i2c_pins(uint8_t sda, uint8_t scl) { +#ifdef USE_ESP_IDF // Temporary until the espressif/esp32-camera library is updated this->config_.pin_sscb_sda = sda; this->config_.pin_sscb_scl = scl; +#else + this->config_.pin_sccb_sda = sda; + this->config_.pin_sccb_scl = scl; +#endif } void ESP32Camera::set_reset_pin(uint8_t pin) { this->config_.pin_reset = pin; } void ESP32Camera::set_power_down_pin(uint8_t pin) { this->config_.pin_pwdn = pin; } @@ -248,6 +280,30 @@ void ESP32Camera::set_frame_size(ESP32CameraFrameSize size) { case ESP32_CAMERA_SIZE_1600X1200: this->config_.frame_size = FRAMESIZE_UXGA; break; + case ESP32_CAMERA_SIZE_1920X1080: + this->config_.frame_size = FRAMESIZE_FHD; + break; + case ESP32_CAMERA_SIZE_720X1280: + this->config_.frame_size = FRAMESIZE_P_HD; + break; + case ESP32_CAMERA_SIZE_864X1536: + this->config_.frame_size = FRAMESIZE_P_3MP; + break; + case ESP32_CAMERA_SIZE_2048X1536: + this->config_.frame_size = FRAMESIZE_QXGA; + break; + case ESP32_CAMERA_SIZE_2560X1440: + this->config_.frame_size = FRAMESIZE_QHD; + break; + case ESP32_CAMERA_SIZE_2560X1600: + this->config_.frame_size = FRAMESIZE_WQXGA; + break; + case ESP32_CAMERA_SIZE_1080X1920: + this->config_.frame_size = FRAMESIZE_P_FHD; + break; + case ESP32_CAMERA_SIZE_2560X1920: + this->config_.frame_size = FRAMESIZE_QSXGA; + break; } } void ESP32Camera::set_jpeg_quality(uint8_t quality) { this->config_.jpeg_quality = quality; } @@ -282,8 +338,20 @@ void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) { void ESP32Camera::add_image_callback(std::function)> &&f) { this->new_image_callback_.add(std::move(f)); } -void ESP32Camera::start_stream(CameraRequester requester) { this->stream_requesters_ |= (1U << requester); } -void ESP32Camera::stop_stream(CameraRequester requester) { this->stream_requesters_ &= ~(1U << requester); } +void ESP32Camera::add_stream_start_callback(std::function &&callback) { + this->stream_start_callback_.add(std::move(callback)); +} +void ESP32Camera::add_stream_stop_callback(std::function &&callback) { + this->stream_stop_callback_.add(std::move(callback)); +} +void ESP32Camera::start_stream(CameraRequester requester) { + this->stream_start_callback_.call(); + this->stream_requesters_ |= (1U << requester); +} +void ESP32Camera::stop_stream(CameraRequester requester) { + this->stream_stop_callback_.call(); + this->stream_requesters_ &= ~(1U << requester); +} void ESP32Camera::request_image(CameraRequester requester) { this->single_requesters_ |= (1U << requester); } void ESP32Camera::update_camera_parameters() { sensor_t *s = esp_camera_sensor_get(); @@ -305,12 +373,11 @@ void ESP32Camera::update_camera_parameters() { s->set_gainceiling(s, (gainceiling_t) this->agc_gain_ceiling_); /* update white balance mode */ s->set_wb_mode(s, (int) this->wb_mode_); // 0 to 4 - /* update test patern */ + /* update test pattern */ s->set_colorbar(s, this->test_pattern_); } /* ---------------- Internal methods ---------------- */ -uint32_t ESP32Camera::hash_base() { return 3010542557UL; } bool ESP32Camera::has_requested_image_() const { return this->single_requesters_ || this->stream_requesters_; } bool ESP32Camera::can_return_image_() const { return this->current_image_.use_count() == 1; } void ESP32Camera::framebuffer_task(void *pv) { diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 743b5bde5f..5f88c6fda8 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -2,6 +2,7 @@ #ifdef USE_ESP32 +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" @@ -28,6 +29,14 @@ enum ESP32CameraFrameSize { ESP32_CAMERA_SIZE_1024X768, // XGA ESP32_CAMERA_SIZE_1280X1024, // SXGA ESP32_CAMERA_SIZE_1600X1200, // UXGA + ESP32_CAMERA_SIZE_1920X1080, // FHD + ESP32_CAMERA_SIZE_720X1280, // PHD + ESP32_CAMERA_SIZE_864X1536, // P3MP + ESP32_CAMERA_SIZE_2048X1536, // QXGA + ESP32_CAMERA_SIZE_2560X1440, // QHD + ESP32_CAMERA_SIZE_2560X1600, // WQXGA + ESP32_CAMERA_SIZE_1080X1920, // PFHD + ESP32_CAMERA_SIZE_2560X1920, // QSXGA }; enum ESP32AgcGainCeiling { @@ -94,7 +103,6 @@ class CameraImageReader { /* ---------------- ESP32Camera class ---------------- */ class ESP32Camera : public Component, public EntityBase { public: - ESP32Camera(const std::string &name); ESP32Camera(); /* setters */ @@ -145,9 +153,11 @@ class ESP32Camera : public Component, public EntityBase { void request_image(CameraRequester requester); void update_camera_parameters(); + void add_stream_start_callback(std::function &&callback); + void add_stream_stop_callback(std::function &&callback); + protected: /* internal methods */ - uint32_t hash_base() override; bool has_requested_image_() const; bool can_return_image_() const; @@ -187,6 +197,8 @@ class ESP32Camera : public Component, public EntityBase { QueueHandle_t framebuffer_get_queue_; QueueHandle_t framebuffer_return_queue_; CallbackManager)> new_image_callback_; + CallbackManager stream_start_callback_{}; + CallbackManager stream_stop_callback_{}; uint32_t last_idle_request_{0}; uint32_t last_update_{0}; @@ -195,6 +207,23 @@ class ESP32Camera : public Component, public EntityBase { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern ESP32Camera *global_esp32_camera; +class ESP32CameraStreamStartTrigger : public Trigger<> { + public: + explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) { + parent->add_stream_start_callback([this]() { this->trigger(); }); + } + + protected: +}; +class ESP32CameraStreamStopTrigger : public Trigger<> { + public: + explicit ESP32CameraStreamStopTrigger(ESP32Camera *parent) { + parent->add_stream_stop_callback([this]() { this->trigger(); }); + } + + protected: +}; + } // namespace esp32_camera } // namespace esphome diff --git a/esphome/components/esp32_can/esp32_can.cpp b/esphome/components/esp32_can/esp32_can.cpp index baae683988..3eb2d1f035 100644 --- a/esphome/components/esp32_can/esp32_can.cpp +++ b/esphome/components/esp32_can/esp32_can.cpp @@ -2,7 +2,7 @@ #include "esp32_can.h" #include "esphome/core/log.h" -#include +#include // WORKAROUND, because CAN_IO_UNUSED is just defined as (-1) in this version // of the framework which does not work with -fpermissive @@ -14,25 +14,25 @@ namespace esp32_can { static const char *const TAG = "esp32_can"; -static bool get_bitrate(canbus::CanSpeed bitrate, can_timing_config_t *t_config) { +static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config) { switch (bitrate) { case canbus::CAN_50KBPS: - *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_50KBITS(); + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_50KBITS(); return true; case canbus::CAN_100KBPS: - *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_100KBITS(); + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_100KBITS(); return true; case canbus::CAN_125KBPS: - *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_125KBITS(); + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_125KBITS(); return true; case canbus::CAN_250KBPS: - *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_250KBITS(); + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_250KBITS(); return true; case canbus::CAN_500KBPS: - *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_500KBITS(); + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_500KBITS(); return true; case canbus::CAN_1000KBPS: - *t_config = (can_timing_config_t) CAN_TIMING_CONFIG_1MBITS(); + *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_1MBITS(); return true; default: return false; @@ -40,10 +40,10 @@ static bool get_bitrate(canbus::CanSpeed bitrate, can_timing_config_t *t_config) } bool ESP32Can::setup_internal() { - can_general_config_t g_config = - CAN_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, CAN_MODE_NORMAL); - can_filter_config_t f_config = CAN_FILTER_CONFIG_ACCEPT_ALL(); - can_timing_config_t t_config; + twai_general_config_t g_config = + TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, TWAI_MODE_NORMAL); + twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); + twai_timing_config_t t_config; if (!get_bitrate(this->bit_rate_, &t_config)) { // invalid bit rate @@ -51,15 +51,15 @@ bool ESP32Can::setup_internal() { return false; } - // Install CAN driver - if (can_driver_install(&g_config, &t_config, &f_config) != ESP_OK) { + // Install TWAI driver + if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) { // Failed to install driver this->mark_failed(); return false; } - // Start CAN driver - if (can_start() != ESP_OK) { + // Start TWAI driver + if (twai_start() != ESP_OK) { // Failed to start driver this->mark_failed(); return false; @@ -72,15 +72,15 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { return canbus::ERROR_FAILTX; } - uint32_t flags = CAN_MSG_FLAG_NONE; + uint32_t flags = TWAI_MSG_FLAG_NONE; if (frame->use_extended_id) { - flags |= CAN_MSG_FLAG_EXTD; + flags |= TWAI_MSG_FLAG_EXTD; } if (frame->remote_transmission_request) { - flags |= CAN_MSG_FLAG_RTR; + flags |= TWAI_MSG_FLAG_RTR; } - can_message_t message = { + twai_message_t message = { .flags = flags, .identifier = frame->can_id, .data_length_code = frame->can_data_length_code, @@ -89,7 +89,7 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { memcpy(message.data, frame->data, frame->can_data_length_code); } - if (can_transmit(&message, pdMS_TO_TICKS(1000)) == ESP_OK) { + if (twai_transmit(&message, pdMS_TO_TICKS(1000)) == ESP_OK) { return canbus::ERROR_OK; } else { return canbus::ERROR_ALLTXBUSY; @@ -97,15 +97,15 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { } canbus::Error ESP32Can::read_message(struct canbus::CanFrame *frame) { - can_message_t message; + twai_message_t message; - if (can_receive(&message, 0) != ESP_OK) { + if (twai_receive(&message, 0) != ESP_OK) { return canbus::ERROR_NOMSG; } frame->can_id = message.identifier; - frame->use_extended_id = message.flags & CAN_MSG_FLAG_EXTD; - frame->remote_transmission_request = message.flags & CAN_MSG_FLAG_RTR; + frame->use_extended_id = message.flags & TWAI_MSG_FLAG_EXTD; + frame->remote_transmission_request = message.flags & TWAI_MSG_FLAG_RTR; frame->can_data_length_code = message.data_length_code; if (!frame->remote_transmission_request) { diff --git a/esphome/components/esp32_hall/sensor.py b/esphome/components/esp32_hall/sensor.py index a752da2c97..0c94224ef8 100644 --- a/esphome/components/esp32_hall/sensor.py +++ b/esphome/components/esp32_hall/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( - CONF_ID, STATE_CLASS_MEASUREMENT, UNIT_MICROTESLA, ICON_MAGNET, @@ -15,23 +14,15 @@ ESP32HallSensor = esp32_hall_ns.class_( "ESP32HallSensor", sensor.Sensor, cg.PollingComponent ) -CONFIG_SCHEMA = ( - sensor.sensor_schema( - unit_of_measurement=UNIT_MICROTESLA, - icon=ICON_MAGNET, - accuracy_decimals=1, - state_class=STATE_CLASS_MEASUREMENT, - ) - .extend( - { - cv.GenerateID(): cv.declare_id(ESP32HallSensor), - } - ) - .extend(cv.polling_component_schema("60s")) -) +CONFIG_SCHEMA = sensor.sensor_schema( + ESP32HallSensor, + unit_of_measurement=UNIT_MICROTESLA, + icon=ICON_MAGNET, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, +).extend(cv.polling_component_schema("60s")) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index c95d4075bc..ae7f0b6427 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -6,7 +6,7 @@ from esphome.const import CONF_ID AUTO_LOAD = ["binary_sensor", "output", "esp32_ble_server"] CODEOWNERS = ["@jesserockz"] -CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"] +CONFLICTS_WITH = ["esp32_ble_beacon"] DEPENDENCIES = ["wifi", "esp32"] CONF_AUTHORIZED_DURATION = "authorized_duration" @@ -22,20 +22,12 @@ ESP32ImprovComponent = esp32_improv_ns.class_( ) -def validate_none_(value): - if value in ("none", "None"): - return None - if cv.boolean(value) is False: - return None - raise cv.Invalid("Must be none") - - CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32ImprovComponent), cv.GenerateID(CONF_BLE_SERVER_ID): cv.use_id(esp32_ble_server.BLEServer), cv.Required(CONF_AUTHORIZER): cv.Any( - validate_none_, cv.use_id(binary_sensor.BinarySensor) + cv.none, cv.use_id(binary_sensor.BinarySensor) ), cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput), cv.Optional( @@ -56,7 +48,7 @@ async def to_code(config): cg.add(ble_server.register_service_component(var)) cg.add_define("USE_IMPROV") - cg.add_library("esphome/Improv", "1.2.0") + cg.add_library("esphome/Improv", "1.2.3") cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION])) cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION])) diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 956934abc1..85013c006b 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -177,8 +177,9 @@ void ESP32ImprovComponent::set_state_(improv::State state) { } void ESP32ImprovComponent::set_error_(improv::Error error) { - if (error != improv::ERROR_NONE) + if (error != improv::ERROR_NONE) { ESP_LOGE(TAG, "Error: %d", error); + } if (this->error_->get_value().empty() || this->error_->get_value()[0] != error) { uint8_t data[1]{error}; this->error_->set_value(data, 1); @@ -194,7 +195,7 @@ void ESP32ImprovComponent::send_response_(std::vector &response) { } void ESP32ImprovComponent::start() { - if (this->state_ != improv::STATE_STOPPED) + if (this->should_start_ || this->state_ != improv::STATE_STOPPED) return; ESP_LOGD(TAG, "Setting Improv to start"); diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 45639f2f63..1a142c94b6 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -9,6 +9,8 @@ #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" +#include + #ifdef USE_ESP32 #include diff --git a/esphome/components/esp32_rmt_led_strip/__init__.py b/esphome/components/esp32_rmt_led_strip/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp new file mode 100644 index 0000000000..df6ee2ce2f --- /dev/null +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -0,0 +1,208 @@ +#include +#include "led_strip.h" + +#ifdef USE_ESP32 + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace esp32_rmt_led_strip { + +static const char *const TAG = "esp32_rmt_led_strip"; + +static const uint8_t RMT_CLK_DIV = 2; + +void ESP32RMTLEDStripLightOutput::setup() { + ESP_LOGCONFIG(TAG, "Setting up ESP32 LED Strip..."); + + size_t buffer_size = this->get_buffer_size_(); + + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->buf_ = allocator.allocate(buffer_size); + if (this->buf_ == nullptr) { + ESP_LOGE(TAG, "Cannot allocate LED buffer!"); + this->mark_failed(); + return; + } + + this->effect_data_ = allocator.allocate(this->num_leds_); + if (this->effect_data_ == nullptr) { + ESP_LOGE(TAG, "Cannot allocate effect data!"); + this->mark_failed(); + return; + } + + ExternalRAMAllocator rmt_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8); // 8 bits per byte, 1 rmt_item32_t per bit + + rmt_config_t config; + memset(&config, 0, sizeof(config)); + config.channel = this->channel_; + config.rmt_mode = RMT_MODE_TX; + config.gpio_num = gpio_num_t(this->pin_); + config.mem_block_num = 1; + config.clk_div = RMT_CLK_DIV; + config.tx_config.loop_en = false; + config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; + config.tx_config.carrier_en = false; + config.tx_config.idle_level = RMT_IDLE_LEVEL_LOW; + config.tx_config.idle_output_en = true; + + if (rmt_config(&config) != ESP_OK) { + ESP_LOGE(TAG, "Cannot initialize RMT!"); + this->mark_failed(); + return; + } + if (rmt_driver_install(config.channel, 0, 0) != ESP_OK) { + ESP_LOGE(TAG, "Cannot install RMT driver!"); + this->mark_failed(); + return; + } +} + +void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, + uint32_t bit1_low) { + float ratio = (float) APB_CLK_FREQ / RMT_CLK_DIV / 1e09f; + + // 0-bit + this->bit0_.duration0 = (uint32_t) (ratio * bit0_high); + this->bit0_.level0 = 1; + this->bit0_.duration1 = (uint32_t) (ratio * bit0_low); + this->bit0_.level1 = 0; + // 1-bit + this->bit1_.duration0 = (uint32_t) (ratio * bit1_high); + this->bit1_.level0 = 1; + this->bit1_.duration1 = (uint32_t) (ratio * bit1_low); + this->bit1_.level1 = 0; +} + +void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { + // protect from refreshing too often + uint32_t now = micros(); + if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) { + // try again next loop iteration, so that this change won't get lost + this->schedule_show(); + return; + } + this->last_refresh_ = now; + this->mark_shown_(); + + ESP_LOGVV(TAG, "Writing RGB values to bus..."); + + if (rmt_wait_tx_done(this->channel_, pdMS_TO_TICKS(1000)) != ESP_OK) { + ESP_LOGE(TAG, "RMT TX timeout"); + this->status_set_warning(); + return; + } + delayMicroseconds(50); + + size_t buffer_size = this->get_buffer_size_(); + + size_t size = 0; + size_t len = 0; + uint8_t *psrc = this->buf_; + rmt_item32_t *pdest = this->rmt_buf_; + while (size < buffer_size) { + uint8_t b = *psrc; + for (int i = 0; i < 8; i++) { + pdest->val = b & (1 << (7 - i)) ? this->bit1_.val : this->bit0_.val; + pdest++; + len++; + } + size++; + psrc++; + } + + if (rmt_write_items(this->channel_, this->rmt_buf_, len, false) != ESP_OK) { + ESP_LOGE(TAG, "RMT TX error"); + this->status_set_warning(); + return; + } + this->status_clear_warning(); +} + +light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index) const { + int32_t r = 0, g = 0, b = 0; + switch (this->rgb_order_) { + case ORDER_RGB: + r = 0; + g = 1; + b = 2; + break; + case ORDER_RBG: + r = 0; + g = 2; + b = 1; + break; + case ORDER_GRB: + r = 1; + g = 0; + b = 2; + break; + case ORDER_GBR: + r = 2; + g = 0; + b = 1; + break; + case ORDER_BGR: + r = 2; + g = 1; + b = 0; + break; + case ORDER_BRG: + r = 1; + g = 2; + b = 0; + break; + } + uint8_t multiplier = this->is_rgbw_ ? 4 : 3; + return {this->buf_ + (index * multiplier) + r, + this->buf_ + (index * multiplier) + g, + this->buf_ + (index * multiplier) + b, + this->is_rgbw_ ? this->buf_ + (index * multiplier) + 3 : nullptr, + &this->effect_data_[index], + &this->correction_}; +} + +void ESP32RMTLEDStripLightOutput::dump_config() { + ESP_LOGCONFIG(TAG, "ESP32 RMT LED Strip:"); + ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); + ESP_LOGCONFIG(TAG, " Channel: %u", this->channel_); + const char *rgb_order; + switch (this->rgb_order_) { + case ORDER_RGB: + rgb_order = "RGB"; + break; + case ORDER_RBG: + rgb_order = "RBG"; + break; + case ORDER_GRB: + rgb_order = "GRB"; + break; + case ORDER_GBR: + rgb_order = "GBR"; + break; + case ORDER_BGR: + rgb_order = "BGR"; + break; + case ORDER_BRG: + rgb_order = "BRG"; + break; + default: + rgb_order = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " RGB Order: %s", rgb_order); + ESP_LOGCONFIG(TAG, " Max refresh rate: %" PRIu32, *this->max_refresh_rate_); + ESP_LOGCONFIG(TAG, " Number of LEDs: %u", this->num_leds_); +} + +float ESP32RMTLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; } + +} // namespace esp32_rmt_led_strip +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.h b/esphome/components/esp32_rmt_led_strip/led_strip.h new file mode 100644 index 0000000000..11d61b07e1 --- /dev/null +++ b/esphome/components/esp32_rmt_led_strip/led_strip.h @@ -0,0 +1,87 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/components/light/addressable_light.h" +#include "esphome/components/light/light_output.h" +#include "esphome/core/color.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include +#include +#include + +namespace esphome { +namespace esp32_rmt_led_strip { + +enum RGBOrder : uint8_t { + ORDER_RGB, + ORDER_RBG, + ORDER_GRB, + ORDER_GBR, + ORDER_BGR, + ORDER_BRG, +}; + +class ESP32RMTLEDStripLightOutput : public light::AddressableLight { + public: + void setup() override; + void write_state(light::LightState *state) override; + float get_setup_priority() const override; + + int32_t size() const override { return this->num_leds_; } + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + if (this->is_rgbw_) { + traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE}); + } else { + traits.set_supported_color_modes({light::ColorMode::RGB}); + } + return traits; + } + + void set_pin(uint8_t pin) { this->pin_ = pin; } + void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; } + void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; } + + /// Set a maximum refresh rate in µs as some lights do not like being updated too often. + void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; } + + void set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, uint32_t bit1_low); + + void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; } + void set_rmt_channel(rmt_channel_t channel) { this->channel_ = channel; } + + void clear_effect_data() override { + for (int i = 0; i < this->size(); i++) + this->effect_data_[i] = 0; + } + + void dump_config() override; + + protected: + light::ESPColorView get_view_internal(int32_t index) const override; + + size_t get_buffer_size_() const { return this->num_leds_ * (3 + this->is_rgbw_); } + + uint8_t *buf_{nullptr}; + uint8_t *effect_data_{nullptr}; + rmt_item32_t *rmt_buf_{nullptr}; + + uint8_t pin_; + uint16_t num_leds_; + bool is_rgbw_; + + rmt_item32_t bit0_, bit1_; + RGBOrder rgb_order_; + rmt_channel_t channel_; + + uint32_t last_refresh_{0}; + optional max_refresh_rate_{}; +}; + +} // namespace esp32_rmt_led_strip +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py new file mode 100644 index 0000000000..3ca758c1e1 --- /dev/null +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -0,0 +1,151 @@ +from dataclasses import dataclass + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import esp32, light +from esphome.const import ( + CONF_CHIPSET, + CONF_MAX_REFRESH_RATE, + CONF_NUM_LEDS, + CONF_OUTPUT_ID, + CONF_PIN, + CONF_RGB_ORDER, +) + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["esp32"] + +esp32_rmt_led_strip_ns = cg.esphome_ns.namespace("esp32_rmt_led_strip") +ESP32RMTLEDStripLightOutput = esp32_rmt_led_strip_ns.class_( + "ESP32RMTLEDStripLightOutput", light.AddressableLight +) + +rmt_channel_t = cg.global_ns.enum("rmt_channel_t") + +RGBOrder = esp32_rmt_led_strip_ns.enum("RGBOrder") + +RGB_ORDERS = { + "RGB": RGBOrder.ORDER_RGB, + "RBG": RGBOrder.ORDER_RBG, + "GRB": RGBOrder.ORDER_GRB, + "GBR": RGBOrder.ORDER_GBR, + "BGR": RGBOrder.ORDER_BGR, + "BRG": RGBOrder.ORDER_BRG, +} + + +@dataclass +class LEDStripTimings: + bit0_high: int + bit0_low: int + bit1_high: int + bit1_low: int + + +CHIPSETS = { + "WS2812": LEDStripTimings(400, 1000, 1000, 400), + "SK6812": LEDStripTimings(300, 900, 600, 600), + "APA106": LEDStripTimings(350, 1360, 1360, 350), + "SM16703": LEDStripTimings(300, 900, 1360, 350), +} + + +CONF_IS_RGBW = "is_rgbw" +CONF_BIT0_HIGH = "bit0_high" +CONF_BIT0_LOW = "bit0_low" +CONF_BIT1_HIGH = "bit1_high" +CONF_BIT1_LOW = "bit1_low" +CONF_RMT_CHANNEL = "rmt_channel" + +RMT_CHANNELS = { + esp32.const.VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7], + esp32.const.VARIANT_ESP32S2: [0, 1, 2, 3], + esp32.const.VARIANT_ESP32S3: [0, 1, 2, 3], + esp32.const.VARIANT_ESP32C3: [0, 1], +} + + +def _validate_rmt_channel(value): + variant = esp32.get_esp32_variant() + if variant not in RMT_CHANNELS: + raise cv.Invalid(f"ESP32 variant {variant} does not support RMT.") + if value not in RMT_CHANNELS[variant]: + raise cv.Invalid( + f"RMT channel {value} is not supported for ESP32 variant {variant}." + ) + return value + + +CONFIG_SCHEMA = cv.All( + light.ADDRESSABLE_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput), + cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, + cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), + cv.Required(CONF_RMT_CHANNEL): _validate_rmt_channel, + cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, + cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), + cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, + cv.Inclusive( + CONF_BIT0_HIGH, + "custom", + ): cv.positive_time_period_microseconds, + cv.Inclusive( + CONF_BIT0_LOW, + "custom", + ): cv.positive_time_period_microseconds, + cv.Inclusive( + CONF_BIT1_HIGH, + "custom", + ): cv.positive_time_period_microseconds, + cv.Inclusive( + CONF_BIT1_LOW, + "custom", + ): cv.positive_time_period_microseconds, + } + ), + cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await light.register_light(var, config) + await cg.register_component(var, config) + + cg.add(var.set_num_leds(config[CONF_NUM_LEDS])) + cg.add(var.set_pin(config[CONF_PIN])) + + if CONF_MAX_REFRESH_RATE in config: + cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE])) + + if CONF_CHIPSET in config: + chipset = CHIPSETS[config[CONF_CHIPSET]] + cg.add( + var.set_led_params( + chipset.bit0_high, + chipset.bit0_low, + chipset.bit1_high, + chipset.bit1_low, + ) + ) + else: + cg.add( + var.set_led_params( + config[CONF_BIT0_HIGH], + config[CONF_BIT0_LOW], + config[CONF_BIT1_HIGH], + config[CONF_BIT1_LOW], + ) + ) + + cg.add(var.set_rgb_order(config[CONF_RGB_ORDER])) + cg.add(var.set_is_rgbw(config[CONF_IS_RGBW])) + + cg.add( + var.set_rmt_channel( + getattr(rmt_channel_t, f"RMT_CHANNEL_{config[CONF_RMT_CHANNEL]}") + ) + ) diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index cdf6aa3abd..3c9bef9665 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_VOLTAGE_ATTENUATION, ) from esphome.core import TimePeriod +from esphome.components import esp32 AUTO_LOAD = ["binary_sensor"] DEPENDENCIES = ["esp32"] @@ -50,30 +51,37 @@ VOLTAGE_ATTENUATION = { "0V": cg.global_ns.TOUCH_HVOLT_ATTEN_0V, } -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(ESP32TouchComponent), - cv.Optional(CONF_SETUP_MODE, default=False): cv.boolean, - cv.Optional( - CONF_IIR_FILTER, default="0ms" - ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_SLEEP_DURATION, default="27306us"): cv.All( - cv.positive_time_period, cv.Range(max=TimePeriod(microseconds=436906)) - ), - cv.Optional(CONF_MEASUREMENT_DURATION, default="8192us"): cv.All( - cv.positive_time_period, cv.Range(max=TimePeriod(microseconds=8192)) - ), - cv.Optional(CONF_LOW_VOLTAGE_REFERENCE, default="0.5V"): validate_voltage( - LOW_VOLTAGE_REFERENCE - ), - cv.Optional(CONF_HIGH_VOLTAGE_REFERENCE, default="2.7V"): validate_voltage( - HIGH_VOLTAGE_REFERENCE - ), - cv.Optional(CONF_VOLTAGE_ATTENUATION, default="0V"): validate_voltage( - VOLTAGE_ATTENUATION - ), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ESP32TouchComponent), + cv.Optional(CONF_SETUP_MODE, default=False): cv.boolean, + cv.Optional( + CONF_IIR_FILTER, default="0ms" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_SLEEP_DURATION, default="27306us"): cv.All( + cv.positive_time_period, cv.Range(max=TimePeriod(microseconds=436906)) + ), + cv.Optional(CONF_MEASUREMENT_DURATION, default="8192us"): cv.All( + cv.positive_time_period, cv.Range(max=TimePeriod(microseconds=8192)) + ), + cv.Optional(CONF_LOW_VOLTAGE_REFERENCE, default="0.5V"): validate_voltage( + LOW_VOLTAGE_REFERENCE + ), + cv.Optional(CONF_HIGH_VOLTAGE_REFERENCE, default="2.7V"): validate_voltage( + HIGH_VOLTAGE_REFERENCE + ), + cv.Optional(CONF_VOLTAGE_ATTENUATION, default="0V"): validate_voltage( + VOLTAGE_ATTENUATION + ), + } + ).extend(cv.COMPONENT_SCHEMA), + esp32.only_on_variant( + supported=[ + esp32.const.VARIANT_ESP32, + ] + ), +) async def to_code(config): diff --git a/esphome/components/esp32_touch/binary_sensor.py b/esphome/components/esp32_touch/binary_sensor.py index bd3e06545d..2cdf1343c3 100644 --- a/esphome/components/esp32_touch/binary_sensor.py +++ b/esphome/components/esp32_touch/binary_sensor.py @@ -1,5 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome.core import CORE from esphome.components import binary_sensor from esphome.const import ( CONF_PIN, @@ -7,6 +8,13 @@ from esphome.const import ( CONF_ID, ) from esphome.components.esp32 import gpio +from esphome.components.esp32.const import ( + KEY_ESP32, + KEY_VARIANT, + VARIANT_ESP32, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) from . import esp32_touch_ns, ESP32TouchComponent DEPENDENCIES = ["esp32_touch", "esp32"] @@ -15,33 +23,71 @@ CONF_ESP32_TOUCH_ID = "esp32_touch_id" CONF_WAKEUP_THRESHOLD = "wakeup_threshold" TOUCH_PADS = { - 4: cg.global_ns.TOUCH_PAD_NUM0, - 0: cg.global_ns.TOUCH_PAD_NUM1, - 2: cg.global_ns.TOUCH_PAD_NUM2, - 15: cg.global_ns.TOUCH_PAD_NUM3, - 13: cg.global_ns.TOUCH_PAD_NUM4, - 12: cg.global_ns.TOUCH_PAD_NUM5, - 14: cg.global_ns.TOUCH_PAD_NUM6, - 27: cg.global_ns.TOUCH_PAD_NUM7, - 33: cg.global_ns.TOUCH_PAD_NUM8, - 32: cg.global_ns.TOUCH_PAD_NUM9, + VARIANT_ESP32: { + 4: cg.global_ns.TOUCH_PAD_NUM0, + 0: cg.global_ns.TOUCH_PAD_NUM1, + 2: cg.global_ns.TOUCH_PAD_NUM2, + 15: cg.global_ns.TOUCH_PAD_NUM3, + 13: cg.global_ns.TOUCH_PAD_NUM4, + 12: cg.global_ns.TOUCH_PAD_NUM5, + 14: cg.global_ns.TOUCH_PAD_NUM6, + 27: cg.global_ns.TOUCH_PAD_NUM7, + 33: cg.global_ns.TOUCH_PAD_NUM8, + 32: cg.global_ns.TOUCH_PAD_NUM9, + }, + VARIANT_ESP32S2: { + 1: cg.global_ns.TOUCH_PAD_NUM1, + 2: cg.global_ns.TOUCH_PAD_NUM2, + 3: cg.global_ns.TOUCH_PAD_NUM3, + 4: cg.global_ns.TOUCH_PAD_NUM4, + 5: cg.global_ns.TOUCH_PAD_NUM5, + 6: cg.global_ns.TOUCH_PAD_NUM6, + 7: cg.global_ns.TOUCH_PAD_NUM7, + 8: cg.global_ns.TOUCH_PAD_NUM8, + 9: cg.global_ns.TOUCH_PAD_NUM9, + 10: cg.global_ns.TOUCH_PAD_NUM10, + 11: cg.global_ns.TOUCH_PAD_NUM11, + 12: cg.global_ns.TOUCH_PAD_NUM12, + 13: cg.global_ns.TOUCH_PAD_NUM13, + 14: cg.global_ns.TOUCH_PAD_NUM14, + }, + VARIANT_ESP32S3: { + 1: cg.global_ns.TOUCH_PAD_NUM1, + 2: cg.global_ns.TOUCH_PAD_NUM2, + 3: cg.global_ns.TOUCH_PAD_NUM3, + 4: cg.global_ns.TOUCH_PAD_NUM4, + 5: cg.global_ns.TOUCH_PAD_NUM5, + 6: cg.global_ns.TOUCH_PAD_NUM6, + 7: cg.global_ns.TOUCH_PAD_NUM7, + 8: cg.global_ns.TOUCH_PAD_NUM8, + 9: cg.global_ns.TOUCH_PAD_NUM9, + 10: cg.global_ns.TOUCH_PAD_NUM10, + 11: cg.global_ns.TOUCH_PAD_NUM11, + 12: cg.global_ns.TOUCH_PAD_NUM12, + 13: cg.global_ns.TOUCH_PAD_NUM13, + 14: cg.global_ns.TOUCH_PAD_NUM14, + }, } def validate_touch_pad(value): value = gpio.validate_gpio_pin(value) - if value not in TOUCH_PADS: + variant = CORE.data[KEY_ESP32][KEY_VARIANT] + if variant not in TOUCH_PADS: + raise cv.Invalid(f"ESP32 variant {variant} does not support touch pads.") + + pads = TOUCH_PADS[variant] + if value not in pads: raise cv.Invalid(f"Pin {value} does not support touch pads.") - return value + return cv.enum(pads)(value) ESP32TouchBinarySensor = esp32_touch_ns.class_( "ESP32TouchBinarySensor", binary_sensor.BinarySensor ) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(ESP32TouchBinarySensor).extend( { - cv.GenerateID(): cv.declare_id(ESP32TouchBinarySensor), cv.GenerateID(CONF_ESP32_TOUCH_ID): cv.use_id(ESP32TouchComponent), cv.Required(CONF_PIN): validate_touch_pad, cv.Required(CONF_THRESHOLD): cv.uint16_t, @@ -54,7 +100,7 @@ async def to_code(config): hub = await cg.get_variable(config[CONF_ESP32_TOUCH_ID]) var = cg.new_Pvariable( config[CONF_ID], - TOUCH_PADS[config[CONF_PIN]], + config[CONF_PIN], config[CONF_THRESHOLD], config[CONF_WAKEUP_THRESHOLD], ) diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index d49e4703a7..c954a14654 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -6,6 +6,8 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #include +#include + #if ESP_IDF_VERSION_MAJOR >= 4 #include #else diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 015caf92e6..674f433d52 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -17,11 +17,18 @@ import esphome.config_validation as cv import esphome.codegen as cg from esphome.helpers import copy_file_if_changed -from .const import CONF_RESTORE_FROM_FLASH, KEY_BOARD, KEY_ESP8266, esp8266_ns -from .boards import ESP8266_FLASH_SIZES, ESP8266_LD_SCRIPTS +from .const import ( + CONF_RESTORE_FROM_FLASH, + CONF_EARLY_PIN_INIT, + KEY_BOARD, + KEY_ESP8266, + KEY_FLASH_SIZE, + KEY_PIN_INITIAL_STATES, + esp8266_ns, +) +from .boards import BOARDS, ESP8266_LD_SCRIPTS -# force import gpio to register pin schema -from .gpio import esp8266_pin_to_code # noqa +from .gpio import PinInitialState, add_pin_initial_states_array CODEOWNERS = ["@esphome/core"] @@ -37,6 +44,9 @@ def set_core_data(config): config[CONF_FRAMEWORK][CONF_VERSION] ) CORE.data[KEY_ESP8266][KEY_BOARD] = config[CONF_BOARD] + CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES] = [ + PinInitialState() for _ in range(16) + ] return config @@ -115,7 +125,7 @@ def _parse_platform_version(value): try: # if platform version is a valid version constraint, prefix the default package cv.platformio_version_constraint(value) - return f"platformio/espressif8266 @ {value}" + return f"platformio/espressif8266@{value}" except cv.Invalid: return value @@ -140,6 +150,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_BOARD): cv.string_strict, cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA, cv.Optional(CONF_RESTORE_FROM_FLASH, default=False): cv.boolean, + cv.Optional(CONF_EARLY_PIN_INIT, default=True): cv.boolean, cv.Optional(CONF_BOARD_FLASH_MODE, default="dout"): cv.one_of( *BUILD_FLASH_MODES, lower=True ), @@ -166,10 +177,11 @@ async def to_code(config): cg.add_platformio_option("framework", "arduino") cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ESP8266_FRAMEWORK_ARDUINO") + cg.add_build_flag("-Wno-nonnull-compare") cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) cg.add_platformio_option( "platform_packages", - [f"platformio/framework-arduinoespressif8266 @ {conf[CONF_SOURCE]}"], + [f"platformio/framework-arduinoespressif8266@{conf[CONF_SOURCE]}"], ) # Default for platformio is LWIP2_LOW_MEMORY with: @@ -188,6 +200,9 @@ async def to_code(config): if config[CONF_RESTORE_FROM_FLASH]: cg.add_define("USE_ESP8266_PREFERENCES_FLASH") + if config[CONF_EARLY_PIN_INIT]: + cg.add_define("USE_ESP8266_EARLY_PIN_INIT") + # Arduino 2 has a non-standards conformant new that returns a nullptr instead of failing when # out of memory and exceptions are disabled. Since Arduino 2.6.0, this flag can be used to make # new abort instead. Use it so that OOM fails early (on allocation) instead of on dereference of @@ -204,8 +219,8 @@ async def to_code(config): cg.RawExpression(f"VERSION_CODE({ver.major}, {ver.minor}, {ver.patch})"), ) - if config[CONF_BOARD] in ESP8266_FLASH_SIZES: - flash_size = ESP8266_FLASH_SIZES[config[CONF_BOARD]] + if config[CONF_BOARD] in BOARDS: + flash_size = BOARDS[config[CONF_BOARD]][KEY_FLASH_SIZE] ld_scripts = ESP8266_LD_SCRIPTS[flash_size] if ver <= cv.Version(2, 3, 0): @@ -220,10 +235,11 @@ async def to_code(config): if ld_script is not None: cg.add_platformio_option("board_build.ldscript", ld_script) + CORE.add_job(add_pin_initial_states_array) + # Called by writer.py def copy_files(): - dir = os.path.dirname(__file__) post_build_file = os.path.join(dir, "post_build.py.script") copy_file_if_changed( diff --git a/esphome/components/esp8266/boards.py b/esphome/components/esp8266/boards.py index 8b0a23a00f..02bfa9e662 100644 --- a/esphome/components/esp8266/boards.py +++ b/esphome/components/esp8266/boards.py @@ -4,50 +4,6 @@ FLASH_SIZE_2_MB = 2 * FLASH_SIZE_1_MB FLASH_SIZE_4_MB = 4 * FLASH_SIZE_1_MB FLASH_SIZE_16_MB = 16 * FLASH_SIZE_1_MB -ESP8266_FLASH_SIZES = { - "d1": FLASH_SIZE_4_MB, - "d1_mini": FLASH_SIZE_4_MB, - "d1_mini_lite": FLASH_SIZE_1_MB, - "d1_mini_pro": FLASH_SIZE_16_MB, - "esp01": FLASH_SIZE_512_KB, - "esp01_1m": FLASH_SIZE_1_MB, - "esp07": FLASH_SIZE_4_MB, - "esp12e": FLASH_SIZE_4_MB, - "esp210": FLASH_SIZE_4_MB, - "esp8285": FLASH_SIZE_1_MB, - "esp_wroom_02": FLASH_SIZE_2_MB, - "espduino": FLASH_SIZE_4_MB, - "espectro": FLASH_SIZE_4_MB, - "espino": FLASH_SIZE_4_MB, - "espinotee": FLASH_SIZE_4_MB, - "espmxdevkit": FLASH_SIZE_1_MB, - "espresso_lite_v1": FLASH_SIZE_4_MB, - "espresso_lite_v2": FLASH_SIZE_4_MB, - "gen4iod": FLASH_SIZE_512_KB, - "heltec_wifi_kit_8": FLASH_SIZE_4_MB, - "huzzah": FLASH_SIZE_4_MB, - "inventone": FLASH_SIZE_4_MB, - "modwifi": FLASH_SIZE_2_MB, - "nodemcu": FLASH_SIZE_4_MB, - "nodemcuv2": FLASH_SIZE_4_MB, - "oak": FLASH_SIZE_4_MB, - "phoenix_v1": FLASH_SIZE_4_MB, - "phoenix_v2": FLASH_SIZE_4_MB, - "sonoff_basic": FLASH_SIZE_1_MB, - "sonoff_s20": FLASH_SIZE_1_MB, - "sonoff_sv": FLASH_SIZE_1_MB, - "sonoff_th": FLASH_SIZE_1_MB, - "sparkfunBlynk": FLASH_SIZE_4_MB, - "thing": FLASH_SIZE_512_KB, - "thingdev": FLASH_SIZE_512_KB, - "wifi_slot": FLASH_SIZE_1_MB, - "wifiduino": FLASH_SIZE_4_MB, - "wifinfo": FLASH_SIZE_1_MB, - "wio_link": FLASH_SIZE_4_MB, - "wio_node": FLASH_SIZE_4_MB, - "xinabox_cw01": FLASH_SIZE_4_MB, -} - ESP8266_LD_SCRIPTS = { FLASH_SIZE_512_KB: ("eagle.flash.512k0.ld", "eagle.flash.512k.ld"), FLASH_SIZE_1_MB: ("eagle.flash.1m0.ld", "eagle.flash.1m.ld"), @@ -206,3 +162,201 @@ ESP8266_BOARD_PINS = { "wio_node": {"LED": 2, "GROVE": 15, "D0": 3, "D1": 5, "BUTTON": 0}, "xinabox_cw01": {"SDA": 2, "SCL": 14, "LED": 5, "LED_RED": 12, "LED_GREEN": 13}, } + +""" +BOARDS generate with: + +git clone https://github.com/platformio/platform-espressif8266 +for x in platform-espressif8266/boards/*.json; do + max_size=$(jq -r .upload.maximum_size <"$x") + name=$(jq -r .name <"$x") + fname=$(basename "$x") + board="${fname%.*}" + size_mb=$((max_size / (1024 * 1024))) + if [[ $size_mb -gt 0 ]]; then + size="${size_mb}_MB" + else + size="${$((max_size / 1024))}_KB" + fi + echo " \"$board\": {\"name\": \"$name\", \"flash_size\": FLASH_SIZE_$size,}," +done | sort +""" + +BOARDS = { + "agruminolemon": { + "name": "Lifely Agrumino Lemon v4", + "flash_size": FLASH_SIZE_4_MB, + }, + "d1_mini_lite": { + "name": "WeMos D1 mini Lite", + "flash_size": FLASH_SIZE_1_MB, + }, + "d1_mini": { + "name": "WeMos D1 R2 and mini", + "flash_size": FLASH_SIZE_4_MB, + }, + "d1_mini_pro": { + "name": "WeMos D1 mini Pro", + "flash_size": FLASH_SIZE_16_MB, + }, + "d1": { + "name": "WEMOS D1 R1", + "flash_size": FLASH_SIZE_4_MB, + }, + "eduinowifi": { + "name": "Schirmilabs Eduino WiFi", + "flash_size": FLASH_SIZE_4_MB, + }, + "esp01_1m": { + "name": "Espressif Generic ESP8266 ESP-01 1M", + "flash_size": FLASH_SIZE_1_MB, + }, + "esp01": { + "name": "Espressif Generic ESP8266 ESP-01 512k", + "flash_size": FLASH_SIZE_512_KB, + }, + "esp07": { + "name": "Espressif Generic ESP8266 ESP-07 1MB", + "flash_size": FLASH_SIZE_1_MB, + }, + "esp07s": { + "name": "Espressif Generic ESP8266 ESP-07S", + "flash_size": FLASH_SIZE_4_MB, + }, + "esp12e": { + "name": "Espressif ESP8266 ESP-12E", + "flash_size": FLASH_SIZE_4_MB, + }, + "esp210": { + "name": "SweetPea ESP-210", + "flash_size": FLASH_SIZE_4_MB, + }, + "esp8285": { + "name": "Generic ESP8285 Module", + "flash_size": FLASH_SIZE_1_MB, + }, + "espduino": { + "name": "ESPDuino (ESP-13 Module)", + "flash_size": FLASH_SIZE_4_MB, + }, + "espectro": { + "name": "ESPectro Core", + "flash_size": FLASH_SIZE_4_MB, + }, + "espino": { + "name": "ESPino", + "flash_size": FLASH_SIZE_4_MB, + }, + "espinotee": { + "name": "ThaiEasyElec ESPino", + "flash_size": FLASH_SIZE_4_MB, + }, + "espmxdevkit": { + "name": "ESP-Mx DevKit (ESP8285)", + "flash_size": FLASH_SIZE_1_MB, + }, + "espresso_lite_v1": { + "name": "ESPresso Lite 1.0", + "flash_size": FLASH_SIZE_4_MB, + }, + "espresso_lite_v2": { + "name": "ESPresso Lite 2.0", + "flash_size": FLASH_SIZE_4_MB, + }, + "esp_wroom_02": { + "name": "ESP-WROOM-02", + "flash_size": FLASH_SIZE_2_MB, + }, + "gen4iod": { + "name": "4D Systems gen4 IoD Range", + "flash_size": FLASH_SIZE_512_KB, + }, + "heltec_wifi_kit_8": { + "name": "Heltec Wifi kit 8", + "flash_size": FLASH_SIZE_4_MB, + }, + "huzzah": { + "name": "Adafruit HUZZAH ESP8266", + "flash_size": FLASH_SIZE_4_MB, + }, + "inventone": { + "name": "Invent One", + "flash_size": FLASH_SIZE_4_MB, + }, + "modwifi": { + "name": "Olimex MOD-WIFI-ESP8266(-DEV)", + "flash_size": FLASH_SIZE_2_MB, + }, + "nodemcu": { + "name": "NodeMCU 0.9 (ESP-12 Module)", + "flash_size": FLASH_SIZE_4_MB, + }, + "nodemcuv2": { + "name": "NodeMCU 1.0 (ESP-12E Module)", + "flash_size": FLASH_SIZE_4_MB, + }, + "oak": { + "name": "DigiStump Oak", + "flash_size": FLASH_SIZE_4_MB, + }, + "phoenix_v1": { + "name": "Phoenix 1.0", + "flash_size": FLASH_SIZE_4_MB, + }, + "phoenix_v2": { + "name": "Phoenix 2.0", + "flash_size": FLASH_SIZE_4_MB, + }, + "sonoff_basic": { + "name": "Sonoff Basic", + "flash_size": FLASH_SIZE_1_MB, + }, + "sonoff_s20": { + "name": "Sonoff S20", + "flash_size": FLASH_SIZE_1_MB, + }, + "sonoff_sv": { + "name": "Sonoff SV", + "flash_size": FLASH_SIZE_1_MB, + }, + "sonoff_th": { + "name": "Sonoff TH", + "flash_size": FLASH_SIZE_1_MB, + }, + "sparkfunBlynk": { + "name": "SparkFun Blynk Board", + "flash_size": FLASH_SIZE_4_MB, + }, + "thingdev": { + "name": "SparkFun ESP8266 Thing Dev", + "flash_size": FLASH_SIZE_512_KB, + }, + "thing": { + "name": "SparkFun ESP8266 Thing", + "flash_size": FLASH_SIZE_512_KB, + }, + "wifiduino": { + "name": "WiFiduino", + "flash_size": FLASH_SIZE_4_MB, + }, + "wifinfo": { + "name": "WifInfo", + "flash_size": FLASH_SIZE_1_MB, + }, + "wifi_slot": { + "name": "WiFi Slot", + "flash_size": FLASH_SIZE_4_MB, + }, + "wio_link": { + "name": "Wio Link", + "flash_size": FLASH_SIZE_4_MB, + }, + "wio_node": { + "name": "Wio Node", + "flash_size": FLASH_SIZE_4_MB, + }, + "xinabox_cw01": { + "name": "XinaBox CW01", + "flash_size": FLASH_SIZE_4_MB, + }, +} diff --git a/esphome/components/esp8266/const.py b/esphome/components/esp8266/const.py index 16a050360c..b718306b01 100644 --- a/esphome/components/esp8266/const.py +++ b/esphome/components/esp8266/const.py @@ -2,7 +2,10 @@ import esphome.codegen as cg KEY_ESP8266 = "esp8266" KEY_BOARD = "board" +KEY_PIN_INITIAL_STATES = "pin_initial_states" CONF_RESTORE_FROM_FLASH = "restore_from_flash" +CONF_EARLY_PIN_INIT = "early_pin_init" +KEY_FLASH_SIZE = "flash_size" # esp8266 namespace is already defined by arduino, manually prefix esphome esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266") diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 828d71a3bd..2d3959b031 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -1,5 +1,7 @@ #ifdef USE_ESP8266 +#include "core.h" +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -53,6 +55,17 @@ extern "C" void resetPins() { // NOLINT // however, not strictly needed as we set up the pins properly // ourselves and this causes pins to toggle during reboot. force_link_symbols(); + +#ifdef USE_ESP8266_EARLY_PIN_INIT + for (int i = 0; i < 16; i++) { + uint8_t mode = ESPHOME_ESP8266_GPIO_INITIAL_MODE[i]; + uint8_t level = ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[i]; + if (mode != 255) + pinMode(i, mode); // NOLINT + if (level != 255) + digitalWrite(i, level); // NOLINT + } +#endif } } // namespace esphome diff --git a/esphome/components/esp8266/core.h b/esphome/components/esp8266/core.h new file mode 100644 index 0000000000..ac33305669 --- /dev/null +++ b/esphome/components/esp8266/core.h @@ -0,0 +1,14 @@ +#pragma once + +#ifdef USE_ESP8266 + +#include + +extern const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16]; +extern const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16]; + +namespace esphome { +namespace esp8266 {} // namespace esp8266 +} // namespace esphome + +#endif // USE_ESP8266 diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index fa5c94dff5..d4b2078524 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -1,4 +1,5 @@ import logging +from dataclasses import dataclass from esphome.const import ( CONF_ID, @@ -12,12 +13,12 @@ from esphome.const import ( CONF_PULLUP, ) from esphome import pins -from esphome.core import CORE +from esphome.core import CORE, coroutine_with_priority import esphome.config_validation as cv import esphome.codegen as cg from . import boards -from .const import KEY_BOARD, KEY_ESP8266, esp8266_ns +from .const import KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, esp8266_ns _LOGGER = logging.getLogger(__name__) @@ -160,11 +161,57 @@ ESP8266_PIN_SCHEMA = cv.All( ) +@dataclass +class PinInitialState: + mode = 255 + level: int = 255 + + @pins.PIN_SCHEMA_REGISTRY.register("esp8266", ESP8266_PIN_SCHEMA) async def esp8266_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] + mode = config[CONF_MODE] cg.add(var.set_pin(num)) cg.add(var.set_inverted(config[CONF_INVERTED])) - cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + cg.add(var.set_flags(pins.gpio_flags_expr(mode))) + if num < 16: + initial_state: PinInitialState = CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES][ + num + ] + if mode[CONF_INPUT]: + if mode[CONF_PULLDOWN]: + initial_state.mode = cg.global_ns.INPUT_PULLDOWN_16 + elif mode[CONF_PULLUP]: + initial_state.mode = cg.global_ns.INPUT_PULLUP + else: + initial_state.mode = cg.global_ns.INPUT + elif mode[CONF_OUTPUT]: + if mode[CONF_OPEN_DRAIN]: + initial_state.mode = cg.global_ns.OUTPUT_OPEN_DRAIN + else: + initial_state.mode = cg.global_ns.OUTPUT + initial_state.level = int(config[CONF_INVERTED]) + return var + + +@coroutine_with_priority(-999.0) +async def add_pin_initial_states_array(): + # Add includes at the very end, so that they override everything + initial_states: list[PinInitialState] = CORE.data[KEY_ESP8266][ + KEY_PIN_INITIAL_STATES + ] + initial_modes_s = ", ".join(str(x.mode) for x in initial_states) + initial_levels_s = ", ".join(str(x.level) for x in initial_states) + + cg.add_global( + cg.RawExpression( + f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16] = {{{initial_modes_s}}}" + ) + ) + cg.add_global( + cg.RawExpression( + f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16] = {{{initial_levels_s}}}" + ) + ) diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index 0e42cea576..8ee5a8225a 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -5,12 +5,14 @@ extern "C" { #include "spi_flash.h" } -#include "preferences.h" -#include -#include "esphome/core/preferences.h" +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/core/defines.h" +#include "esphome/core/preferences.h" +#include "preferences.h" + +#include +#include namespace esphome { namespace esp8266 { @@ -243,17 +245,34 @@ class ESP8266Preferences : public ESPPreferences { } } if (erase_res != SPI_FLASH_RESULT_OK) { - ESP_LOGV(TAG, "Erase ESP8266 flash failed!"); + ESP_LOGE(TAG, "Erase ESP8266 flash failed!"); return false; } if (write_res != SPI_FLASH_RESULT_OK) { - ESP_LOGV(TAG, "Write ESP8266 flash failed!"); + ESP_LOGE(TAG, "Write ESP8266 flash failed!"); return false; } s_flash_dirty = false; return true; } + + bool reset() override { + ESP_LOGD(TAG, "Cleaning up preferences in flash..."); + SpiFlashOpResult erase_res; + { + InterruptLock lock; + erase_res = spi_flash_erase_sector(get_esp8266_flash_sector()); + } + if (erase_res != SPI_FLASH_RESULT_OK) { + ESP_LOGE(TAG, "Erase ESP8266 flash failed!"); + return false; + } + + // Protect flash from writing till restart + s_prevent_write = true; + return true; + } }; void setup_preferences() { diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index bbf64a3cd1..bedc0a4c30 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -30,15 +30,32 @@ CONF_POWER_PIN = "power_pin" EthernetType = ethernet_ns.enum("EthernetType") ETHERNET_TYPES = { "LAN8720": EthernetType.ETHERNET_TYPE_LAN8720, - "TLK110": EthernetType.ETHERNET_TYPE_TLK110, + "RTL8201": EthernetType.ETHERNET_TYPE_RTL8201, + "DP83848": EthernetType.ETHERNET_TYPE_DP83848, + "IP101": EthernetType.ETHERNET_TYPE_IP101, + "JL1101": EthernetType.ETHERNET_TYPE_JL1101, + "KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081, } -eth_clock_mode_t = cg.global_ns.enum("eth_clock_mode_t") +emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t") +emac_rmii_clock_gpio_t = cg.global_ns.enum("emac_rmii_clock_gpio_t") CLK_MODES = { - "GPIO0_IN": eth_clock_mode_t.ETH_CLOCK_GPIO0_IN, - "GPIO0_OUT": eth_clock_mode_t.ETH_CLOCK_GPIO0_OUT, - "GPIO16_OUT": eth_clock_mode_t.ETH_CLOCK_GPIO16_OUT, - "GPIO17_OUT": eth_clock_mode_t.ETH_CLOCK_GPIO17_OUT, + "GPIO0_IN": ( + emac_rmii_clock_mode_t.EMAC_CLK_EXT_IN, + emac_rmii_clock_gpio_t.EMAC_CLK_IN_GPIO, + ), + "GPIO0_OUT": ( + emac_rmii_clock_mode_t.EMAC_CLK_OUT, + emac_rmii_clock_gpio_t.EMAC_APPL_CLK_OUT_GPIO, + ), + "GPIO16_OUT": ( + emac_rmii_clock_mode_t.EMAC_CLK_OUT, + emac_rmii_clock_gpio_t.EMAC_CLK_OUT_GPIO, + ), + "GPIO17_OUT": ( + emac_rmii_clock_mode_t.EMAC_CLK_OUT, + emac_rmii_clock_gpio_t.EMAC_CLK_OUT_180_GPIO, + ), } @@ -77,7 +94,7 @@ CONFIG_SCHEMA = cv.All( CLK_MODES, upper=True, space="_" ), cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31), - cv.Optional(CONF_POWER_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number, cv.Optional(CONF_MANUAL_IP): MANUAL_IP_SCHEMA, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, @@ -88,7 +105,6 @@ CONFIG_SCHEMA = cv.All( } ).extend(cv.COMPONENT_SCHEMA), _validate, - cv.only_with_arduino, ) @@ -112,17 +128,16 @@ async def to_code(config): cg.add(var.set_mdc_pin(config[CONF_MDC_PIN])) cg.add(var.set_mdio_pin(config[CONF_MDIO_PIN])) cg.add(var.set_type(config[CONF_TYPE])) - cg.add(var.set_clk_mode(CLK_MODES[config[CONF_CLK_MODE]])) + cg.add(var.set_clk_mode(*CLK_MODES[config[CONF_CLK_MODE]])) cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) if CONF_POWER_PIN in config: - pin = await cg.gpio_pin_expression(config[CONF_POWER_PIN]) - cg.add(var.set_power_pin(pin)) + cg.add(var.set_power_pin(config[CONF_POWER_PIN])) if CONF_MANUAL_IP in config: cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP]))) cg.add_define("USE_ETHERNET") - if CORE.is_esp32: + if CORE.using_arduino: cg.add_library("WiFi", None) diff --git a/esphome/components/ethernet/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c new file mode 100644 index 0000000000..6011795033 --- /dev/null +++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c @@ -0,0 +1,339 @@ +// Copyright 2019 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifdef USE_ESP32 + +#include +#include +#include +#include "esp_log.h" +#include "esp_eth.h" +#include "eth_phy_regs_struct.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/gpio.h" +#include "esp_rom_gpio.h" +#include "esp_rom_sys.h" + +static const char *TAG = "jl1101"; +#define PHY_CHECK(a, str, goto_tag, ...) \ + do { \ + if (!(a)) { \ + ESP_LOGE(TAG, "%s(%d): " str, __FUNCTION__, __LINE__, ##__VA_ARGS__); \ + goto goto_tag; \ + } \ + } while (0) + +/***************Vendor Specific Register***************/ + +/** + * @brief PSR(Page Select Register) + * + */ +typedef union { + struct { + uint16_t page_select : 8; /* Select register page, default is 0 */ + uint16_t reserved : 8; /* Reserved */ + }; + uint16_t val; +} psr_reg_t; +#define ETH_PHY_PSR_REG_ADDR (0x1F) + +typedef struct { + esp_eth_phy_t parent; + esp_eth_mediator_t *eth; + int addr; + uint32_t reset_timeout_ms; + uint32_t autonego_timeout_ms; + eth_link_t link_status; + int reset_gpio_num; +} phy_jl1101_t; + +static esp_err_t jl1101_page_select(phy_jl1101_t *jl1101, uint32_t page) { + esp_eth_mediator_t *eth = jl1101->eth; + psr_reg_t psr = {.page_select = page}; + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_PSR_REG_ADDR, psr.val) == ESP_OK, "write PSR failed", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_update_link_duplex_speed(phy_jl1101_t *jl1101) { + esp_eth_mediator_t *eth = jl1101->eth; + eth_speed_t speed = ETH_SPEED_10M; + eth_duplex_t duplex = ETH_DUPLEX_HALF; + bmcr_reg_t bmcr; + bmsr_reg_t bmsr; + uint32_t peer_pause_ability = false; + anlpar_reg_t anlpar; + PHY_CHECK(jl1101_page_select(jl1101, 0) == ESP_OK, "select page 0 failed", err); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMSR_REG_ADDR, &(bmsr.val)) == ESP_OK, "read BMSR failed", + err); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_ANLPAR_REG_ADDR, &(anlpar.val)) == ESP_OK, + "read ANLPAR failed", err); + eth_link_t link = bmsr.link_status ? ETH_LINK_UP : ETH_LINK_DOWN; + /* check if link status changed */ + if (jl1101->link_status != link) { + /* when link up, read negotiation result */ + if (link == ETH_LINK_UP) { + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + if (bmcr.speed_select) { + speed = ETH_SPEED_100M; + } else { + speed = ETH_SPEED_10M; + } + if (bmcr.duplex_mode) { + duplex = ETH_DUPLEX_FULL; + } else { + duplex = ETH_DUPLEX_HALF; + } + PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_SPEED, (void *) speed) == ESP_OK, "change speed failed", err); + PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_DUPLEX, (void *) duplex) == ESP_OK, "change duplex failed", err); + /* if we're in duplex mode, and peer has the flow control ability */ + if (duplex == ETH_DUPLEX_FULL && anlpar.symmetric_pause) { + peer_pause_ability = 1; + } else { + peer_pause_ability = 0; + } + PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_PAUSE, (void *) peer_pause_ability) == ESP_OK, + "change pause ability failed", err); + } + PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_LINK, (void *) link) == ESP_OK, "change link failed", err); + jl1101->link_status = link; + } + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_set_mediator(esp_eth_phy_t *phy, esp_eth_mediator_t *eth) { + PHY_CHECK(eth, "can't set mediator to null", err); + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + jl1101->eth = eth; + return ESP_OK; +err: + return ESP_ERR_INVALID_ARG; +} + +static esp_err_t jl1101_get_link(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + /* Updata information about link, speed, duplex */ + PHY_CHECK(jl1101_update_link_duplex_speed(jl1101) == ESP_OK, "update link duplex speed failed", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_reset(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + jl1101->link_status = ETH_LINK_DOWN; + esp_eth_mediator_t *eth = jl1101->eth; + bmcr_reg_t bmcr = {.reset = 1}; + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, bmcr.val) == ESP_OK, "write BMCR failed", err); + /* Wait for reset complete */ + uint32_t to = 0; + for (to = 0; to < jl1101->reset_timeout_ms / 50; to++) { + vTaskDelay(pdMS_TO_TICKS(50)); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + if (!bmcr.reset) { + break; + } + } + PHY_CHECK(to < jl1101->reset_timeout_ms / 50, "reset timeout", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_reset_hw(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + if (jl1101->reset_gpio_num >= 0) { + esp_rom_gpio_pad_select_gpio(jl1101->reset_gpio_num); + gpio_set_direction(jl1101->reset_gpio_num, GPIO_MODE_OUTPUT); + gpio_set_level(jl1101->reset_gpio_num, 0); + esp_rom_delay_us(100); // insert min input assert time + gpio_set_level(jl1101->reset_gpio_num, 1); + } + return ESP_OK; +} + +static esp_err_t jl1101_negotiate(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + esp_eth_mediator_t *eth = jl1101->eth; + /* in case any link status has changed, let's assume we're in link down status */ + jl1101->link_status = ETH_LINK_DOWN; + /* Restart auto negotiation */ + bmcr_reg_t bmcr = { + .speed_select = 1, /* 100Mbps */ + .duplex_mode = 1, /* Full Duplex */ + .en_auto_nego = 1, /* Auto Negotiation */ + .restart_auto_nego = 1 /* Restart Auto Negotiation */ + }; + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, bmcr.val) == ESP_OK, "write BMCR failed", err); + /* Wait for auto negotiation complete */ + bmsr_reg_t bmsr; + uint32_t to = 0; + for (to = 0; to < jl1101->autonego_timeout_ms / 100; to++) { + vTaskDelay(pdMS_TO_TICKS(100)); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMSR_REG_ADDR, &(bmsr.val)) == ESP_OK, "read BMSR failed", + err); + if (bmsr.auto_nego_complete) { + break; + } + } + /* Auto negotiation failed, maybe no network cable plugged in, so output a warning */ + if (to >= jl1101->autonego_timeout_ms / 100) { + ESP_LOGW(TAG, "auto negotiation timeout"); + } + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_pwrctl(esp_eth_phy_t *phy, bool enable) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + esp_eth_mediator_t *eth = jl1101->eth; + bmcr_reg_t bmcr; + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + if (!enable) { + /* Enable IEEE Power Down Mode */ + bmcr.power_down = 1; + } else { + /* Disable IEEE Power Down Mode */ + bmcr.power_down = 0; + } + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, bmcr.val) == ESP_OK, "write BMCR failed", err); + if (!enable) { + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + PHY_CHECK(bmcr.power_down == 1, "power down failed", err); + } else { + /* wait for power up complete */ + uint32_t to = 0; + for (to = 0; to < jl1101->reset_timeout_ms / 10; to++) { + vTaskDelay(pdMS_TO_TICKS(10)); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed", + err); + if (bmcr.power_down == 0) { + break; + } + } + PHY_CHECK(to < jl1101->reset_timeout_ms / 10, "power up timeout", err); + } + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_set_addr(esp_eth_phy_t *phy, uint32_t addr) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + jl1101->addr = addr; + return ESP_OK; +} + +static esp_err_t jl1101_get_addr(esp_eth_phy_t *phy, uint32_t *addr) { + PHY_CHECK(addr, "addr can't be null", err); + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + *addr = jl1101->addr; + return ESP_OK; +err: + return ESP_ERR_INVALID_ARG; +} + +static esp_err_t jl1101_del(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + free(jl1101); + return ESP_OK; +} + +static esp_err_t jl1101_advertise_pause_ability(esp_eth_phy_t *phy, uint32_t ability) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + esp_eth_mediator_t *eth = jl1101->eth; + /* Set PAUSE function ability */ + anar_reg_t anar; + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_ANAR_REG_ADDR, &(anar.val)) == ESP_OK, "read ANAR failed", + err); + if (ability) { + anar.asymmetric_pause = 1; + anar.symmetric_pause = 1; + } else { + anar.asymmetric_pause = 0; + anar.symmetric_pause = 0; + } + PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_ANAR_REG_ADDR, anar.val) == ESP_OK, "write ANAR failed", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_init(esp_eth_phy_t *phy) { + phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); + esp_eth_mediator_t *eth = jl1101->eth; + // Detect PHY address + if (jl1101->addr == ESP_ETH_PHY_ADDR_AUTO) { + PHY_CHECK(esp_eth_detect_phy_addr(eth, &jl1101->addr) == ESP_OK, "Detect PHY address failed", err); + } + /* Power on Ethernet PHY */ + PHY_CHECK(jl1101_pwrctl(phy, true) == ESP_OK, "power control failed", err); + /* Reset Ethernet PHY */ + PHY_CHECK(jl1101_reset(phy) == ESP_OK, "reset failed", err); + /* Check PHY ID */ + phyidr1_reg_t id1; + phyidr2_reg_t id2; + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_IDR1_REG_ADDR, &(id1.val)) == ESP_OK, "read ID1 failed", err); + PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_IDR2_REG_ADDR, &(id2.val)) == ESP_OK, "read ID2 failed", err); + PHY_CHECK(id1.oui_msb == 0x937C && id2.oui_lsb == 0x10 && id2.vendor_model == 0x2, "wrong chip ID", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +static esp_err_t jl1101_deinit(esp_eth_phy_t *phy) { + /* Power off Ethernet PHY */ + PHY_CHECK(jl1101_pwrctl(phy, false) == ESP_OK, "power control failed", err); + return ESP_OK; +err: + return ESP_FAIL; +} + +esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config) { + PHY_CHECK(config, "can't set phy config to null", err); + phy_jl1101_t *jl1101 = calloc(1, sizeof(phy_jl1101_t)); + PHY_CHECK(jl1101, "calloc jl1101 failed", err); + jl1101->addr = config->phy_addr; + jl1101->reset_gpio_num = config->reset_gpio_num; + jl1101->reset_timeout_ms = config->reset_timeout_ms; + jl1101->link_status = ETH_LINK_DOWN; + jl1101->autonego_timeout_ms = config->autonego_timeout_ms; + jl1101->parent.reset = jl1101_reset; + jl1101->parent.reset_hw = jl1101_reset_hw; + jl1101->parent.init = jl1101_init; + jl1101->parent.deinit = jl1101_deinit; + jl1101->parent.set_mediator = jl1101_set_mediator; + jl1101->parent.negotiate = jl1101_negotiate; + jl1101->parent.get_link = jl1101_get_link; + jl1101->parent.pwrctl = jl1101_pwrctl; + jl1101->parent.get_addr = jl1101_get_addr; + jl1101->parent.set_addr = jl1101_set_addr; + jl1101->parent.advertise_pause_ability = jl1101_advertise_pause_ability; + jl1101->parent.del = jl1101_del; + + return &(jl1101->parent); +err: + return NULL; +} +#endif /* USE_ESP32 */ diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 384a31ed2f..0487ea5498 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -3,20 +3,10 @@ #include "esphome/core/util.h" #include "esphome/core/application.h" -#ifdef USE_ESP32_FRAMEWORK_ARDUINO +#ifdef USE_ESP32 -#include -#include #include - -/// Macro for IDF version comparison -#ifndef ESP_IDF_VERSION_VAL -#define ESP_IDF_VERSION_VAL(major, minor, patch) (((major) << 16) | ((minor) << 8) | (patch)) -#endif - -// Defined in WiFiGeneric.cpp, sets global initialized flag, starts network event task queue and calls -// tcpip_adapter_init() -extern void tcpipInit(); // NOLINT(readability-identifier-naming) +#include "esp_event.h" namespace esphome { namespace ethernet { @@ -33,23 +23,60 @@ EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non- } EthernetComponent::EthernetComponent() { global_eth_component = this; } + void EthernetComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up Ethernet..."); - - auto f = std::bind(&EthernetComponent::on_wifi_event_, this, std::placeholders::_1, std::placeholders::_2); - WiFi.onEvent(f); - - if (this->power_pin_ != nullptr) { - this->power_pin_->setup(); + if (esp_reset_reason() != ESP_RST_DEEPSLEEP) { + // Delay here to allow power to stabilise before Ethernet is initialized. + delay(300); // NOLINT } + esp_err_t err; + err = esp_netif_init(); + ESPHL_ERROR_CHECK(err, "ETH netif init error"); + err = esp_event_loop_create_default(); + ESPHL_ERROR_CHECK(err, "ETH event loop error"); + + esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH(); + this->eth_netif_ = esp_netif_new(&cfg); + + // Init MAC and PHY configs to default + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); + eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); + + phy_config.phy_addr = this->phy_addr_; + phy_config.reset_gpio_num = this->power_pin_; + + mac_config.smi_mdc_gpio_num = this->mdc_pin_; + mac_config.smi_mdio_gpio_num = this->mdio_pin_; + mac_config.clock_config.rmii.clock_mode = this->clk_mode_; + mac_config.clock_config.rmii.clock_gpio = this->clk_gpio_; + + esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&mac_config); + switch (this->type_) { case ETHERNET_TYPE_LAN8720: { - memcpy(&this->eth_config_, &phy_lan8720_default_ethernet_config, sizeof(eth_config_t)); + this->phy_ = esp_eth_phy_new_lan87xx(&phy_config); break; } - case ETHERNET_TYPE_TLK110: { - memcpy(&this->eth_config_, &phy_tlk110_default_ethernet_config, sizeof(eth_config_t)); + case ETHERNET_TYPE_RTL8201: { + this->phy_ = esp_eth_phy_new_rtl8201(&phy_config); + break; + } + case ETHERNET_TYPE_DP83848: { + this->phy_ = esp_eth_phy_new_dp83848(&phy_config); + break; + } + case ETHERNET_TYPE_IP101: { + this->phy_ = esp_eth_phy_new_ip101(&phy_config); + break; + } + case ETHERNET_TYPE_JL1101: { + this->phy_ = esp_eth_phy_new_jl1101(&phy_config); + break; + } + case ETHERNET_TYPE_KSZ8081: { + this->phy_ = esp_eth_phy_new_ksz8081(&phy_config); break; } default: { @@ -58,24 +85,25 @@ void EthernetComponent::setup() { } } - this->eth_config_.phy_addr = static_cast(this->phy_addr_); - this->eth_config_.clock_mode = this->clk_mode_; - this->eth_config_.gpio_config = EthernetComponent::eth_phy_config_gpio; - this->eth_config_.tcpip_input = tcpip_adapter_eth_input; + esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, this->phy_); + this->eth_handle_ = nullptr; + err = esp_eth_driver_install(ð_config, &this->eth_handle_); + ESPHL_ERROR_CHECK(err, "ETH driver install error"); + /* attach Ethernet driver to TCP/IP stack */ + err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_)); + ESPHL_ERROR_CHECK(err, "ETH netif attach error"); - if (this->power_pin_ != nullptr) { - this->orig_power_enable_fun_ = this->eth_config_.phy_power_enable; - this->eth_config_.phy_power_enable = EthernetComponent::eth_phy_power_enable; - } + // Register user defined event handers + err = esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &EthernetComponent::eth_event_handler, nullptr); + ESPHL_ERROR_CHECK(err, "ETH event handler register error"); + err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &EthernetComponent::got_ip_event_handler, nullptr); + ESPHL_ERROR_CHECK(err, "GOT IP event handler register error"); - tcpipInit(); - - esp_err_t err; - err = esp_eth_init(&this->eth_config_); - ESPHL_ERROR_CHECK(err, "ETH init error"); - err = esp_eth_enable(); - ESPHL_ERROR_CHECK(err, "ETH enable error"); + /* start Ethernet driver state machine */ + err = esp_eth_start(this->eth_handle_); + ESPHL_ERROR_CHECK(err, "ETH start error"); } + void EthernetComponent::loop() { const uint32_t now = millis(); @@ -115,45 +143,79 @@ void EthernetComponent::loop() { break; } } + void EthernetComponent::dump_config() { + const char *eth_type; + switch (this->type_) { + case ETHERNET_TYPE_LAN8720: + eth_type = "LAN8720"; + break; + + case ETHERNET_TYPE_RTL8201: + eth_type = "RTL8201"; + break; + + case ETHERNET_TYPE_DP83848: + eth_type = "DP83848"; + break; + + case ETHERNET_TYPE_IP101: + eth_type = "IP101"; + break; + + case ETHERNET_TYPE_JL1101: + eth_type = "JL1101"; + break; + + case ETHERNET_TYPE_KSZ8081: + eth_type = "KSZ8081"; + break; + + default: + eth_type = "Unknown"; + break; + } + ESP_LOGCONFIG(TAG, "Ethernet:"); this->dump_connect_params_(); - LOG_PIN(" Power Pin: ", this->power_pin_); + if (this->power_pin_ != -1) { + ESP_LOGCONFIG(TAG, " Power Pin: %u", this->power_pin_); + } ESP_LOGCONFIG(TAG, " MDC Pin: %u", this->mdc_pin_); ESP_LOGCONFIG(TAG, " MDIO Pin: %u", this->mdio_pin_); - ESP_LOGCONFIG(TAG, " Type: %s", this->type_ == ETHERNET_TYPE_LAN8720 ? "LAN8720" : "TLK110"); + ESP_LOGCONFIG(TAG, " Type: %s", eth_type); + ESP_LOGCONFIG(TAG, " PHY addr: %u", this->phy_addr_); } + float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; } + bool EthernetComponent::can_proceed() { return this->is_connected(); } + network::IPAddress EthernetComponent::get_ip_address() { - tcpip_adapter_ip_info_t ip; - tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_ETH, &ip); + esp_netif_ip_info_t ip; + esp_netif_get_ip_info(this->eth_netif_, &ip); return {ip.ip.addr}; } -void EthernetComponent::on_wifi_event_(system_event_id_t event, system_event_info_t info) { +void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event, void *event_data) { const char *event_name; switch (event) { - case SYSTEM_EVENT_ETH_START: + case ETHERNET_EVENT_START: event_name = "ETH started"; - this->started_ = true; + global_eth_component->started_ = true; break; - case SYSTEM_EVENT_ETH_STOP: + case ETHERNET_EVENT_STOP: event_name = "ETH stopped"; - this->started_ = false; - this->connected_ = false; + global_eth_component->started_ = false; + global_eth_component->connected_ = false; break; - case SYSTEM_EVENT_ETH_CONNECTED: + case ETHERNET_EVENT_CONNECTED: event_name = "ETH connected"; break; - case SYSTEM_EVENT_ETH_DISCONNECTED: + case ETHERNET_EVENT_DISCONNECTED: event_name = "ETH disconnected"; - this->connected_ = false; - break; - case SYSTEM_EVENT_ETH_GOT_IP: - event_name = "ETH Got IP"; - this->connected_ = true; + global_eth_component->connected_ = false; break; default: return; @@ -162,17 +224,23 @@ void EthernetComponent::on_wifi_event_(system_event_id_t event, system_event_inf ESP_LOGV(TAG, "[Ethernet event] %s (num=%d)", event_name, event); } +void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, + void *event_data) { + global_eth_component->connected_ = true; + ESP_LOGV(TAG, "[Ethernet event] ETH Got IP (num=%d)", event_id); +} + void EthernetComponent::start_connect_() { this->connect_begin_ = millis(); this->status_set_warning(); esp_err_t err; - err = tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_ETH, App.get_name().c_str()); + err = esp_netif_set_hostname(this->eth_netif_, App.get_name().c_str()); if (err != ERR_OK) { - ESP_LOGW(TAG, "tcpip_adapter_set_hostname failed: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err)); } - tcpip_adapter_ip_info_t info; + esp_netif_ip_info_t info; if (this->manual_ip_.has_value()) { info.ip.addr = static_cast(this->manual_ip_->static_ip); info.gw.addr = static_cast(this->manual_ip_->gateway); @@ -183,29 +251,45 @@ void EthernetComponent::start_connect_() { info.netmask.addr = 0; } - err = tcpip_adapter_dhcpc_stop(TCPIP_ADAPTER_IF_ETH); - if (err != ESP_ERR_TCPIP_ADAPTER_DHCP_ALREADY_STOPPED) { + esp_netif_dhcp_status_t status = ESP_NETIF_DHCP_INIT; + + err = esp_netif_dhcpc_get_status(this->eth_netif_, &status); + ESPHL_ERROR_CHECK(err, "DHCPC Get Status Failed!"); + + ESP_LOGV(TAG, "DHCP Client Status: %d", status); + + err = esp_netif_dhcpc_stop(this->eth_netif_); + if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) { ESPHL_ERROR_CHECK(err, "DHCPC stop error"); } - err = tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_ETH, &info); + + err = esp_netif_set_ip_info(this->eth_netif_, &info); ESPHL_ERROR_CHECK(err, "DHCPC set IP info error"); if (this->manual_ip_.has_value()) { if (uint32_t(this->manual_ip_->dns1) != 0) { ip_addr_t d; +#if LWIP_IPV6 d.type = IPADDR_TYPE_V4; d.u_addr.ip4.addr = static_cast(this->manual_ip_->dns1); +#else + d.addr = static_cast(this->manual_ip_->dns1); +#endif dns_setserver(0, &d); } - if (uint32_t(this->manual_ip_->dns1) != 0) { + if (uint32_t(this->manual_ip_->dns2) != 0) { ip_addr_t d; +#if LWIP_IPV6 d.type = IPADDR_TYPE_V4; d.u_addr.ip4.addr = static_cast(this->manual_ip_->dns2); +#else + d.addr = static_cast(this->manual_ip_->dns2); +#endif dns_setserver(1, &d); } } else { - err = tcpip_adapter_dhcpc_start(TCPIP_ADAPTER_IF_ETH); - if (err != ESP_ERR_TCPIP_ADAPTER_DHCP_ALREADY_STARTED) { + err = esp_netif_dhcpc_start(this->eth_netif_); + if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) { ESPHL_ERROR_CHECK(err, "DHCPC start error"); } } @@ -213,59 +297,82 @@ void EthernetComponent::start_connect_() { this->connect_begin_ = millis(); this->status_set_warning(); } -void EthernetComponent::eth_phy_config_gpio() { - phy_rmii_configure_data_interface_pins(); - phy_rmii_smi_configure_pins(global_eth_component->mdc_pin_, global_eth_component->mdio_pin_); -} -void EthernetComponent::eth_phy_power_enable(bool enable) { - global_eth_component->power_pin_->digital_write(enable); - // power up takes some time, datasheet says max 300µs - delay(1); - global_eth_component->orig_power_enable_fun_(enable); -} + bool EthernetComponent::is_connected() { return this->state_ == EthernetComponentState::CONNECTED; } + void EthernetComponent::dump_connect_params_() { - tcpip_adapter_ip_info_t ip; - tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_ETH, &ip); + esp_netif_ip_info_t ip; + esp_netif_get_ip_info(this->eth_netif_, &ip); ESP_LOGCONFIG(TAG, " IP Address: %s", network::IPAddress(ip.ip.addr).str().c_str()); ESP_LOGCONFIG(TAG, " Hostname: '%s'", App.get_name().c_str()); ESP_LOGCONFIG(TAG, " Subnet: %s", network::IPAddress(ip.netmask.addr).str().c_str()); ESP_LOGCONFIG(TAG, " Gateway: %s", network::IPAddress(ip.gw.addr).str().c_str()); -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 4) const ip_addr_t *dns_ip1 = dns_getserver(0); const ip_addr_t *dns_ip2 = dns_getserver(1); -#else - ip_addr_t tmp_ip1 = dns_getserver(0); - const ip_addr_t *dns_ip1 = &tmp_ip1; - ip_addr_t tmp_ip2 = dns_getserver(1); - const ip_addr_t *dns_ip2 = &tmp_ip2; -#endif + +#if LWIP_IPV6 ESP_LOGCONFIG(TAG, " DNS1: %s", network::IPAddress(dns_ip1->u_addr.ip4.addr).str().c_str()); ESP_LOGCONFIG(TAG, " DNS2: %s", network::IPAddress(dns_ip2->u_addr.ip4.addr).str().c_str()); +#else + ESP_LOGCONFIG(TAG, " DNS1: %s", network::IPAddress(dns_ip1->addr).str().c_str()); + ESP_LOGCONFIG(TAG, " DNS2: %s", network::IPAddress(dns_ip2->addr).str().c_str()); +#endif + + esp_err_t err; + uint8_t mac[6]; - esp_eth_get_mac(mac); + err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, &mac); + ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error"); ESP_LOGCONFIG(TAG, " MAC Address: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - ESP_LOGCONFIG(TAG, " Is Full Duplex: %s", YESNO(this->eth_config_.phy_get_duplex_mode())); - ESP_LOGCONFIG(TAG, " Link Up: %s", YESNO(this->eth_config_.phy_check_link())); - ESP_LOGCONFIG(TAG, " Link Speed: %u", this->eth_config_.phy_get_speed_mode() ? 100 : 10); + + eth_duplex_t duplex_mode; + err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode); + ESPHL_ERROR_CHECK(err, "ETH_CMD_G_DUPLEX_MODE error"); + ESP_LOGCONFIG(TAG, " Is Full Duplex: %s", YESNO(duplex_mode == ETH_DUPLEX_FULL)); + + eth_speed_t speed; + err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed); + ESPHL_ERROR_CHECK(err, "ETH_CMD_G_SPEED error"); + ESP_LOGCONFIG(TAG, " Link Speed: %u", speed == ETH_SPEED_100M ? 100 : 10); } + void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; } -void EthernetComponent::set_power_pin(GPIOPin *power_pin) { this->power_pin_ = power_pin; } +void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; } void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; } void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; } void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } -void EthernetComponent::set_clk_mode(eth_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; } +void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode, emac_rmii_clock_gpio_t clk_gpio) { + this->clk_mode_ = clk_mode; + this->clk_gpio_ = clk_gpio; +} void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } + std::string EthernetComponent::get_use_address() const { if (this->use_address_.empty()) { return App.get_name() + ".local"; } return this->use_address_; } + void EthernetComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } +bool EthernetComponent::powerdown() { + ESP_LOGI(TAG, "Powering down ethernet PHY"); + if (this->phy_ == nullptr) { + ESP_LOGE(TAG, "Ethernet PHY not assigned"); + return false; + } + this->connected_ = false; + this->started_ = false; + if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) { + ESP_LOGE(TAG, "Error powering down ethernet PHY"); + return false; + } + return true; +} + } // namespace ethernet } // namespace esphome -#endif // USE_ESP32_FRAMEWORK_ARDUINO +#endif // USE_ESP32 diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index abe1c62030..918e47212f 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -1,22 +1,26 @@ #pragma once -#ifdef USE_ESP32_FRAMEWORK_ARDUINO - #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/components/network/ip_address.h" +#ifdef USE_ESP32 + #include "esp_eth.h" -#include -#include -#include +#include "esp_eth_mac.h" +#include "esp_netif.h" namespace esphome { namespace ethernet { enum EthernetType { - ETHERNET_TYPE_LAN8720 = 0, - ETHERNET_TYPE_TLK110, + ETHERNET_TYPE_UNKNOWN = 0, + ETHERNET_TYPE_LAN8720, + ETHERNET_TYPE_RTL8201, + ETHERNET_TYPE_DP83848, + ETHERNET_TYPE_IP101, + ETHERNET_TYPE_JL1101, + ETHERNET_TYPE_KSZ8081, }; struct ManualIP { @@ -41,49 +45,53 @@ class EthernetComponent : public Component { void dump_config() override; float get_setup_priority() const override; bool can_proceed() override; + void on_shutdown() override { powerdown(); } bool is_connected(); void set_phy_addr(uint8_t phy_addr); - void set_power_pin(GPIOPin *power_pin); + void set_power_pin(int power_pin); void set_mdc_pin(uint8_t mdc_pin); void set_mdio_pin(uint8_t mdio_pin); void set_type(EthernetType type); - void set_clk_mode(eth_clock_mode_t clk_mode); + void set_clk_mode(emac_rmii_clock_mode_t clk_mode, emac_rmii_clock_gpio_t clk_gpio); void set_manual_ip(const ManualIP &manual_ip); network::IPAddress get_ip_address(); std::string get_use_address() const; void set_use_address(const std::string &use_address); + bool powerdown(); protected: - void on_wifi_event_(system_event_id_t event, system_event_info_t info); + static void eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); + static void got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); + void start_connect_(); void dump_connect_params_(); - static void eth_phy_config_gpio(); - static void eth_phy_power_enable(bool enable); - std::string use_address_; uint8_t phy_addr_{0}; - GPIOPin *power_pin_{nullptr}; + int power_pin_{-1}; uint8_t mdc_pin_{23}; uint8_t mdio_pin_{18}; - EthernetType type_{ETHERNET_TYPE_LAN8720}; - eth_clock_mode_t clk_mode_{ETH_CLOCK_GPIO0_IN}; + EthernetType type_{ETHERNET_TYPE_UNKNOWN}; + emac_rmii_clock_mode_t clk_mode_{EMAC_CLK_EXT_IN}; + emac_rmii_clock_gpio_t clk_gpio_{EMAC_CLK_IN_GPIO}; optional manual_ip_{}; bool started_{false}; bool connected_{false}; EthernetComponentState state_{EthernetComponentState::STOPPED}; uint32_t connect_begin_; - eth_config_t eth_config_; - eth_phy_power_enable_func orig_power_enable_fun_; + esp_netif_t *eth_netif_{nullptr}; + esp_eth_handle_t eth_handle_; + esp_eth_phy_t *phy_{nullptr}; }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern EthernetComponent *global_eth_component; +extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); } // namespace ethernet } // namespace esphome -#endif // USE_ESP32_FRAMEWORK_ARDUINO +#endif // USE_ESP32 diff --git a/esphome/components/ethernet_info/__init__.py b/esphome/components/ethernet_info/__init__.py new file mode 100644 index 0000000000..312688d3fd --- /dev/null +++ b/esphome/components/ethernet_info/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@gtjadsonsantos"] diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp new file mode 100644 index 0000000000..f841875396 --- /dev/null +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp @@ -0,0 +1,16 @@ +#include "ethernet_info_text_sensor.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace ethernet_info { + +static const char *const TAG = "ethernet_info"; + +void IPAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo IPAddress", this); } + +} // namespace ethernet_info +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.h b/esphome/components/ethernet_info/ethernet_info_text_sensor.h new file mode 100644 index 0000000000..2d46fe18eb --- /dev/null +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/ethernet/ethernet_component.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace ethernet_info { + +class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextSensor { + public: + void update() override { + auto ip = ethernet::global_eth_component->get_ip_address(); + if (ip != this->last_ip_) { + this->last_ip_ = ip; + this->publish_state(network::IPAddress(ip).str()); + } + } + + float get_setup_priority() const override { return setup_priority::ETHERNET; } + std::string unique_id() override { return get_mac_address() + "-ethernetinfo"; } + void dump_config() override; + + protected: + network::IPAddress last_ip_; +}; + +} // namespace ethernet_info +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/ethernet_info/text_sensor.py b/esphome/components/ethernet_info/text_sensor.py new file mode 100644 index 0000000000..7cb9944c92 --- /dev/null +++ b/esphome/components/ethernet_info/text_sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import ( + CONF_IP_ADDRESS, + ENTITY_CATEGORY_DIAGNOSTIC, +) + +DEPENDENCIES = ["ethernet"] + +ethernet_info_ns = cg.esphome_ns.namespace("ethernet_info") + +IPAddressEsthernetInfo = ethernet_info_ns.class_( + "IPAddressEthernetInfo", text_sensor.TextSensor, cg.PollingComponent +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema( + IPAddressEsthernetInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ).extend(cv.polling_component_schema("1s")) + } +) + + +async def setup_conf(config, key): + if key in config: + conf = config[key] + var = await text_sensor.new_text_sensor(conf) + await cg.register_component(var, conf) + + +async def to_code(config): + await setup_conf(config, CONF_IP_ADDRESS) diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py index d0153f6104..bbb703dc5c 100644 --- a/esphome/components/external_components/__init__.py +++ b/esphome/components/external_components/__init__.py @@ -1,90 +1,32 @@ -import re import logging from pathlib import Path import esphome.config_validation as cv +from esphome import git, loader from esphome.const import ( CONF_COMPONENTS, + CONF_EXTERNAL_COMPONENTS, + CONF_PASSWORD, + CONF_PATH, CONF_REF, CONF_REFRESH, CONF_SOURCE, - CONF_URL, CONF_TYPE, - CONF_EXTERNAL_COMPONENTS, - CONF_PATH, + CONF_URL, CONF_USERNAME, - CONF_PASSWORD, + TYPE_GIT, + TYPE_LOCAL, ) from esphome.core import CORE -from esphome import git, loader _LOGGER = logging.getLogger(__name__) DOMAIN = CONF_EXTERNAL_COMPONENTS -TYPE_GIT = "git" -TYPE_LOCAL = "local" - - -GIT_SCHEMA = { - cv.Required(CONF_URL): cv.url, - cv.Optional(CONF_REF): cv.git_ref, - cv.Optional(CONF_USERNAME): cv.string, - cv.Optional(CONF_PASSWORD): cv.string, -} -LOCAL_SCHEMA = { - cv.Required(CONF_PATH): cv.directory, -} - - -def validate_source_shorthand(value): - if not isinstance(value, str): - raise cv.Invalid("Shorthand only for strings") - try: - return SOURCE_SCHEMA({CONF_TYPE: TYPE_LOCAL, CONF_PATH: value}) - except cv.Invalid: - pass - # Regex for GitHub repo name with optional branch/tag - # Note: git allows other branch/tag names as well, but never seen them used before - m = re.match( - r"github://(?:([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)(?:@([a-zA-Z0-9\-_.\./]+))?|pr#([0-9]+))", - value, - ) - if m is None: - raise cv.Invalid( - "Source is not a file system path, in expected github://username/name[@branch-or-tag] or github://pr#1234 format!" - ) - if m.group(4): - conf = { - CONF_TYPE: TYPE_GIT, - CONF_URL: "https://github.com/esphome/esphome.git", - CONF_REF: f"pull/{m.group(4)}/head", - } - else: - conf = { - CONF_TYPE: TYPE_GIT, - CONF_URL: f"https://github.com/{m.group(1)}/{m.group(2)}.git", - } - if m.group(3): - conf[CONF_REF] = m.group(3) - - return SOURCE_SCHEMA(conf) - - -SOURCE_SCHEMA = cv.Any( - validate_source_shorthand, - cv.typed_schema( - { - TYPE_GIT: cv.Schema(GIT_SCHEMA), - TYPE_LOCAL: cv.Schema(LOCAL_SCHEMA), - } - ), -) - CONFIG_SCHEMA = cv.ensure_list( { - cv.Required(CONF_SOURCE): SOURCE_SCHEMA, + cv.Required(CONF_SOURCE): cv.SOURCE_SCHEMA, cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, cv.source_refresh), cv.Optional(CONF_COMPONENTS, default="all"): cv.Any( "all", cv.ensure_list(cv.string) @@ -98,7 +40,7 @@ async def to_code(config): def _process_git_config(config: dict, refresh) -> str: - repo_dir = git.clone_or_update( + repo_dir, _ = git.clone_or_update( url=config[CONF_URL], ref=config.get(CONF_REF), refresh=refresh, diff --git a/esphome/components/ezo/automation.h b/esphome/components/ezo/automation.h new file mode 100644 index 0000000000..19427b9159 --- /dev/null +++ b/esphome/components/ezo/automation.h @@ -0,0 +1,53 @@ +#pragma once +#include + +#include "esphome/core/automation.h" +#include "ezo.h" + +namespace esphome { +namespace ezo { + +class LedTrigger : public Trigger { + public: + explicit LedTrigger(EZOSensor *ezo) { + ezo->add_led_state_callback([this](bool value) { this->trigger(value); }); + } +}; + +class CustomTrigger : public Trigger { + public: + explicit CustomTrigger(EZOSensor *ezo) { + ezo->add_custom_callback([this](std::string value) { this->trigger(std::move(value)); }); + } +}; + +class TTrigger : public Trigger { + public: + explicit TTrigger(EZOSensor *ezo) { + ezo->add_t_callback([this](std::string value) { this->trigger(std::move(value)); }); + } +}; + +class CalibrationTrigger : public Trigger { + public: + explicit CalibrationTrigger(EZOSensor *ezo) { + ezo->add_calibration_callback([this](std::string value) { this->trigger(std::move(value)); }); + } +}; + +class SlopeTrigger : public Trigger { + public: + explicit SlopeTrigger(EZOSensor *ezo) { + ezo->add_slope_callback([this](std::string value) { this->trigger(std::move(value)); }); + } +}; + +class DeviceInformationTrigger : public Trigger { + public: + explicit DeviceInformationTrigger(EZOSensor *ezo) { + ezo->add_device_infomation_callback([this](std::string value) { this->trigger(std::move(value)); }); + } +}; + +} // namespace ezo +} // namespace esphome diff --git a/esphome/components/ezo/ezo.cpp b/esphome/components/ezo/ezo.cpp index 12f88a0f66..8e4486dbf2 100644 --- a/esphome/components/ezo/ezo.cpp +++ b/esphome/components/ezo/ezo.cpp @@ -5,52 +5,91 @@ namespace esphome { namespace ezo { -static const char *const TAG = "ezo.sensor"; +static const char *const EZO_COMMAND_TYPE_STRINGS[] = {"EZO_READ", "EZO_LED", "EZO_DEVICE_INFORMATION", + "EZO_SLOPE", "EZO_CALIBRATION", "EZO_SLEEP", + "EZO_I2C", "EZO_T", "EZO_CUSTOM"}; -static const uint16_t EZO_STATE_WAIT = 1; -static const uint16_t EZO_STATE_SEND_TEMP = 2; -static const uint16_t EZO_STATE_WAIT_TEMP = 4; +static const char *const EZO_CALIBRATION_TYPE_STRINGS[] = {"LOW", "MID", "HIGH"}; void EZOSensor::dump_config() { LOG_SENSOR("", "EZO", this); LOG_I2C_DEVICE(this); - if (this->is_failed()) + if (this->is_failed()) { ESP_LOGE(TAG, "Communication with EZO circuit failed!"); + } LOG_UPDATE_INTERVAL(this); } void EZOSensor::update() { - if (this->state_ & EZO_STATE_WAIT) { - ESP_LOGE(TAG, "update overrun, still waiting for previous response"); + // Check if a read is in there already and if not insert on in the second position + + if (!this->commands_.empty() && this->commands_.front()->command_type != EzoCommandType::EZO_READ && + this->commands_.size() > 1) { + bool found = false; + + for (auto &i : this->commands_) { + if (i->command_type == EzoCommandType::EZO_READ) { + found = true; + break; + } + } + + if (!found) { + std::unique_ptr ezo_command(new EzoCommand); + ezo_command->command = "R"; + ezo_command->command_type = EzoCommandType::EZO_READ; + ezo_command->delay_ms = 900; + + auto it = this->commands_.begin(); + ++it; + this->commands_.insert(it, std::move(ezo_command)); + } + return; } - uint8_t c = 'R'; - this->write(&c, 1); - this->state_ |= EZO_STATE_WAIT; - this->start_time_ = millis(); - this->wait_time_ = 900; + + this->get_state(); } void EZOSensor::loop() { - uint8_t buf[21]; - if (!(this->state_ & EZO_STATE_WAIT)) { - if (this->state_ & EZO_STATE_SEND_TEMP) { - int len = sprintf((char *) buf, "T,%0.3f", this->tempcomp_); - this->write(buf, len); - this->state_ = EZO_STATE_WAIT | EZO_STATE_WAIT_TEMP; - this->start_time_ = millis(); - this->wait_time_ = 300; + if (this->commands_.empty()) { + return; + } + + EzoCommand *to_run = this->commands_.front().get(); + + if (!to_run->command_sent) { + const uint8_t *data = reinterpret_cast(to_run->command.c_str()); + ESP_LOGVV(TAG, "Sending command \"%s\"", data); + + this->write(data, to_run->command.length()); + + if (to_run->command_type == EzoCommandType::EZO_SLEEP || + to_run->command_type == EzoCommandType::EZO_I2C) { // Commands with no return data + this->commands_.pop_front(); + if (to_run->command_type == EzoCommandType::EZO_I2C) + this->address_ = this->new_address_; + return; } + + this->start_time_ = millis(); + to_run->command_sent = true; return; } - if (millis() - this->start_time_ < this->wait_time_) + + if (millis() - this->start_time_ < to_run->delay_ms) return; + + uint8_t buf[32]; + buf[0] = 0; - if (!this->read_bytes_raw(buf, 20)) { + + if (!this->read_bytes_raw(buf, 32)) { ESP_LOGE(TAG, "read error"); - this->state_ = 0; + this->commands_.pop_front(); return; } + switch (buf[0]) { case 1: break; @@ -66,28 +105,143 @@ void EZOSensor::loop() { ESP_LOGE(TAG, "device returned an unknown response: %d", buf[0]); break; } - if (this->state_ & EZO_STATE_WAIT_TEMP) { - this->state_ = 0; - return; - } - this->state_ &= ~EZO_STATE_WAIT; - if (buf[0] != 1) - return; - // some sensors return multiple comma-separated values, terminate string after first one - for (size_t i = 1; i < sizeof(buf) - 1; i++) { - if (buf[i] == ',') - buf[i] = '\0'; + ESP_LOGV(TAG, "Received buffer \"%s\" for command type %s", &buf[1], EZO_COMMAND_TYPE_STRINGS[to_run->command_type]); + + if (buf[0] == 1) { + std::string payload = reinterpret_cast(&buf[1]); + if (!payload.empty()) { + switch (to_run->command_type) { + case EzoCommandType::EZO_READ: { + // some sensors return multiple comma-separated values, terminate string after first one + int start_location = 0; + if ((start_location = payload.find(',')) != std::string::npos) { + payload.erase(start_location); + } + auto val = parse_number(payload); + if (!val.has_value()) { + ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str()); + } else { + this->publish_state(*val); + } + break; + } + case EzoCommandType::EZO_LED: { + this->led_callback_.call(payload.back() == '1'); + break; + } + case EzoCommandType::EZO_DEVICE_INFORMATION: { + int start_location = 0; + if ((start_location = payload.find(',')) != std::string::npos) { + this->device_infomation_callback_.call(payload.substr(start_location + 1)); + } + break; + } + case EzoCommandType::EZO_SLOPE: { + int start_location = 0; + if ((start_location = payload.find(',')) != std::string::npos) { + this->slope_callback_.call(payload.substr(start_location + 1)); + } + break; + } + case EzoCommandType::EZO_CALIBRATION: { + int start_location = 0; + if ((start_location = payload.find(',')) != std::string::npos) { + this->calibration_callback_.call(payload.substr(start_location + 1)); + } + break; + } + case EzoCommandType::EZO_T: { + int start_location = 0; + if ((start_location = payload.find(',')) != std::string::npos) { + this->t_callback_.call(payload.substr(start_location + 1)); + } + break; + } + case EzoCommandType::EZO_CUSTOM: { + this->custom_callback_.call(payload); + break; + } + default: { + break; + } + } + } } - float val = parse_number((char *) &buf[1]).value_or(0); - this->publish_state(val); + this->commands_.pop_front(); } -void EZOSensor::set_tempcomp_value(float temp) { - this->tempcomp_ = temp; - this->state_ |= EZO_STATE_SEND_TEMP; +void EZOSensor::add_command_(const std::string &command, EzoCommandType command_type, uint16_t delay_ms) { + std::unique_ptr ezo_command(new EzoCommand); + ezo_command->command = command; + ezo_command->command_type = command_type; + ezo_command->delay_ms = delay_ms; + this->commands_.push_back(std::move(ezo_command)); +}; + +void EZOSensor::set_calibration_point_(EzoCalibrationType type, float value) { + std::string payload = str_sprintf("Cal,%s,%0.2f", EZO_CALIBRATION_TYPE_STRINGS[type], value); + this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900); } +void EZOSensor::set_address(uint8_t address) { + if (address > 0 && address < 128) { + std::string payload = str_sprintf("I2C,%u", address); + this->new_address_ = address; + this->add_command_(payload, EzoCommandType::EZO_I2C); + } else { + ESP_LOGE(TAG, "Invalid I2C address"); + } +} + +void EZOSensor::get_device_information() { this->add_command_("i", EzoCommandType::EZO_DEVICE_INFORMATION); } + +void EZOSensor::set_sleep() { this->add_command_("Sleep", EzoCommandType::EZO_SLEEP); } + +void EZOSensor::get_state() { this->add_command_("R", EzoCommandType::EZO_READ, 900); } + +void EZOSensor::get_slope() { this->add_command_("Slope,?", EzoCommandType::EZO_SLOPE); } + +void EZOSensor::get_t() { this->add_command_("T,?", EzoCommandType::EZO_T); } + +void EZOSensor::set_t(float value) { + std::string payload = str_sprintf("T,%0.2f", value); + this->add_command_(payload, EzoCommandType::EZO_T); +} + +void EZOSensor::set_tempcomp_value(float temp) { this->set_t(temp); } + +void EZOSensor::get_calibration() { this->add_command_("Cal,?", EzoCommandType::EZO_CALIBRATION); } + +void EZOSensor::set_calibration_point_low(float value) { + this->set_calibration_point_(EzoCalibrationType::EZO_CAL_LOW, value); +} + +void EZOSensor::set_calibration_point_mid(float value) { + this->set_calibration_point_(EzoCalibrationType::EZO_CAL_MID, value); +} + +void EZOSensor::set_calibration_point_high(float value) { + this->set_calibration_point_(EzoCalibrationType::EZO_CAL_HIGH, value); +} + +void EZOSensor::set_calibration_generic(float value) { + std::string payload = str_sprintf("Cal,%0.2f", value); + this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900); +} + +void EZOSensor::clear_calibration() { this->add_command_("Cal,clear", EzoCommandType::EZO_CALIBRATION); } + +void EZOSensor::get_led_state() { this->add_command_("L,?", EzoCommandType::EZO_LED); } + +void EZOSensor::set_led_state(bool on) { + std::string to_send = "L,"; + to_send += on ? "1" : "0"; + this->add_command_(to_send, EzoCommandType::EZO_LED); +} + +void EZOSensor::send_custom(const std::string &to_send) { this->add_command_(to_send, EzoCommandType::EZO_CUSTOM); } + } // namespace ezo } // namespace esphome diff --git a/esphome/components/ezo/ezo.h b/esphome/components/ezo/ezo.h index d46d193ae7..28b46643e9 100644 --- a/esphome/components/ezo/ezo.h +++ b/esphome/components/ezo/ezo.h @@ -3,10 +3,35 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include namespace esphome { namespace ezo { +static const char *const TAG = "ezo.sensor"; + +enum EzoCommandType : uint8_t { + EZO_READ = 0, + EZO_LED, + EZO_DEVICE_INFORMATION, + EZO_SLOPE, + EZO_CALIBRATION, + EZO_SLEEP, + EZO_I2C, + EZO_T, + EZO_CUSTOM +}; + +enum EzoCalibrationType : uint8_t { EZO_CAL_LOW = 0, EZO_CAL_MID = 1, EZO_CAL_HIGH = 2 }; + +class EzoCommand { + public: + std::string command; + uint16_t delay_ms = 0; + bool command_sent = false; + EzoCommandType command_type; +}; + /// This class implements support for the EZO circuits in i2c mode class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { public: @@ -15,13 +40,71 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2 void update() override; float get_setup_priority() const override { return setup_priority::DATA; }; - void set_tempcomp_value(float temp); + // I2C + void set_address(uint8_t address); + + // Device Information + void get_device_information(); + void add_device_infomation_callback(std::function &&callback) { + this->device_infomation_callback_.add(std::move(callback)); + } + + // Sleep + void set_sleep(); + + // R + void get_state(); + + // Slope + void get_slope(); + void add_slope_callback(std::function &&callback) { + this->slope_callback_.add(std::move(callback)); + } + + // T + void get_t(); + void set_t(float value); + void set_tempcomp_value(float temp); // For backwards compatibility + void add_t_callback(std::function &&callback) { this->t_callback_.add(std::move(callback)); } + + // Calibration + void get_calibration(); + void set_calibration_point_low(float value); + void set_calibration_point_mid(float value); + void set_calibration_point_high(float value); + void set_calibration_generic(float value); + void clear_calibration(); + void add_calibration_callback(std::function &&callback) { + this->calibration_callback_.add(std::move(callback)); + } + + // LED + void get_led_state(); + void set_led_state(bool on); + void add_led_state_callback(std::function &&callback) { this->led_callback_.add(std::move(callback)); } + + // Custom + void send_custom(const std::string &to_send); + void add_custom_callback(std::function &&callback) { + this->custom_callback_.add(std::move(callback)); + } protected: + std::deque> commands_; + int new_address_; + + void add_command_(const std::string &command, EzoCommandType command_type, uint16_t delay_ms = 300); + + void set_calibration_point_(EzoCalibrationType type, float value); + + CallbackManager device_infomation_callback_{}; + CallbackManager calibration_callback_{}; + CallbackManager slope_callback_{}; + CallbackManager t_callback_{}; + CallbackManager custom_callback_{}; + CallbackManager led_callback_{}; + uint32_t start_time_ = 0; - uint32_t wait_time_ = 0; - uint16_t state_ = 0; - float tempcomp_; }; } // namespace ezo diff --git a/esphome/components/ezo/sensor.py b/esphome/components/ezo/sensor.py index 09b36b7135..486ba0126e 100644 --- a/esphome/components/ezo/sensor.py +++ b/esphome/components/ezo/sensor.py @@ -1,22 +1,81 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome import automation from esphome.components import i2c, sensor -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_TRIGGER_ID CODEOWNERS = ["@ssieb"] DEPENDENCIES = ["i2c"] +CONF_ON_LED = "on_led" +CONF_ON_DEVICE_INFORMATION = "on_device_information" +CONF_ON_SLOPE = "on_slope" +CONF_ON_CALIBRATION = "on_calibration" +CONF_ON_T = "on_t" +CONF_ON_CUSTOM = "on_custom" + ezo_ns = cg.esphome_ns.namespace("ezo") EZOSensor = ezo_ns.class_( "EZOSensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice ) +CustomTrigger = ezo_ns.class_( + "CustomTrigger", automation.Trigger.template(cg.std_string) +) + + +TTrigger = ezo_ns.class_("TTrigger", automation.Trigger.template(cg.std_string)) + +SlopeTrigger = ezo_ns.class_("SlopeTrigger", automation.Trigger.template(cg.std_string)) + +CalibrationTrigger = ezo_ns.class_( + "CalibrationTrigger", automation.Trigger.template(cg.std_string) +) + +DeviceInformationTrigger = ezo_ns.class_( + "DeviceInformationTrigger", automation.Trigger.template(cg.std_string) +) + +LedTrigger = ezo_ns.class_("LedTrigger", automation.Trigger.template(cg.bool_)) + CONFIG_SCHEMA = ( - sensor.SENSOR_SCHEMA.extend( + sensor.sensor_schema(EZOSensor) + .extend( { - cv.GenerateID(): cv.declare_id(EZOSensor), + cv.Optional(CONF_ON_CUSTOM): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CustomTrigger), + } + ), + cv.Optional(CONF_ON_CALIBRATION): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CalibrationTrigger), + } + ), + cv.Optional(CONF_ON_SLOPE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SlopeTrigger), + } + ), + cv.Optional(CONF_ON_T): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TTrigger), + } + ), + cv.Optional(CONF_ON_DEVICE_INFORMATION): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + DeviceInformationTrigger + ), + } + ), + cv.Optional(CONF_ON_LED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LedTrigger), + } + ), } ) .extend(cv.polling_component_schema("60s")) @@ -29,3 +88,27 @@ async def to_code(config): await cg.register_component(var, config) await sensor.register_sensor(var, config) await i2c.register_i2c_device(var, config) + + for conf in config.get(CONF_ON_CUSTOM, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + for conf in config.get(CONF_ON_LED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(bool, "x")], conf) + + for conf in config.get(CONF_ON_DEVICE_INFORMATION, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + for conf in config.get(CONF_ON_SLOPE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + for conf in config.get(CONF_ON_CALIBRATION, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + for conf in config.get(CONF_ON_T, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) diff --git a/esphome/components/ezo_pmp/__init__.py b/esphome/components/ezo_pmp/__init__.py new file mode 100644 index 0000000000..e65fcf74ca --- /dev/null +++ b/esphome/components/ezo_pmp/__init__.py @@ -0,0 +1,291 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ADDRESS, CONF_COMMAND, CONF_ID, CONF_DURATION +from esphome import automation +from esphome.automation import maybe_simple_id + +CODEOWNERS = ["@carlos-sarmiento"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +CONF_VOLUME = "volume" +CONF_VOLUME_PER_MINUTE = "volume_per_minute" + +ezo_pmp_ns = cg.esphome_ns.namespace("ezo_pmp") +EzoPMP = ezo_pmp_ns.class_("EzoPMP", cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(EzoPMP), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(103)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + +EZO_PMP_NO_ARGS_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + } +) + +# Actions that do not require more arguments + +EzoPMPFindAction = ezo_pmp_ns.class_("EzoPMPFindAction", automation.Action) +EzoPMPClearTotalVolumeDispensedAction = ezo_pmp_ns.class_( + "EzoPMPClearTotalVolumeDispensedAction", automation.Action +) +EzoPMPClearCalibrationAction = ezo_pmp_ns.class_( + "EzoPMPClearCalibrationAction", automation.Action +) +EzoPMPPauseDosingAction = ezo_pmp_ns.class_( + "EzoPMPPauseDosingAction", automation.Action +) +EzoPMPStopDosingAction = ezo_pmp_ns.class_("EzoPMPStopDosingAction", automation.Action) +EzoPMPDoseContinuouslyAction = ezo_pmp_ns.class_( + "EzoPMPDoseContinuouslyAction", automation.Action +) + +# Actions that require more arguments +EzoPMPDoseVolumeAction = ezo_pmp_ns.class_("EzoPMPDoseVolumeAction", automation.Action) +EzoPMPDoseVolumeOverTimeAction = ezo_pmp_ns.class_( + "EzoPMPDoseVolumeOverTimeAction", automation.Action +) +EzoPMPDoseWithConstantFlowRateAction = ezo_pmp_ns.class_( + "EzoPMPDoseWithConstantFlowRateAction", automation.Action +) +EzoPMPSetCalibrationVolumeAction = ezo_pmp_ns.class_( + "EzoPMPSetCalibrationVolumeAction", automation.Action +) +EzoPMPChangeI2CAddressAction = ezo_pmp_ns.class_( + "EzoPMPChangeI2CAddressAction", automation.Action +) +EzoPMPArbitraryCommandAction = ezo_pmp_ns.class_( + "EzoPMPArbitraryCommandAction", automation.Action +) + + +@automation.register_action( + "ezo_pmp.find", EzoPMPFindAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA +) +async def ezo_pmp_find_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action( + "ezo_pmp.dose_continuously", + EzoPMPDoseContinuouslyAction, + EZO_PMP_NO_ARGS_ACTION_SCHEMA, +) +async def ezo_pmp_dose_continuously_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action( + "ezo_pmp.clear_total_volume_dosed", + EzoPMPClearTotalVolumeDispensedAction, + EZO_PMP_NO_ARGS_ACTION_SCHEMA, +) +async def ezo_pmp_clear_total_volume_dosed_to_code( + config, action_id, template_arg, args +): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action( + "ezo_pmp.clear_calibration", + EzoPMPClearCalibrationAction, + EZO_PMP_NO_ARGS_ACTION_SCHEMA, +) +async def ezo_pmp_clear_calibration_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action( + "ezo_pmp.pause_dosing", EzoPMPPauseDosingAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA +) +async def ezo_pmp_pause_dosing_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action( + "ezo_pmp.stop_dosing", EzoPMPStopDosingAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA +) +async def ezo_pmp_stop_dosing_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +# Actions that require Multiple Args + +EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + cv.Required(CONF_VOLUME): cv.templatable( + cv.float_range() + ), # Any way to represent as proper volume (vs. raw int) + } +) + + +@automation.register_action( + "ezo_pmp.dose_volume", EzoPMPDoseVolumeAction, EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA +) +async def ezo_pmp_dose_volume_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + template_ = await cg.templatable(config[CONF_VOLUME], args, cg.double) + cg.add(var.set_volume(template_)) + + return var + + +EZO_PMP_DOSE_VOLUME_OVER_TIME_ACTION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + cv.Required(CONF_VOLUME): cv.templatable( + cv.float_range() + ), # Any way to represent as proper volume (vs. raw int) + cv.Required(CONF_DURATION): cv.templatable( + cv.int_range(1) + ), # Any way to represent it as minutes (vs. raw int) + } +) + + +@automation.register_action( + "ezo_pmp.dose_volume_over_time", + EzoPMPDoseVolumeOverTimeAction, + EZO_PMP_DOSE_VOLUME_OVER_TIME_ACTION_SCHEMA, +) +async def ezo_pmp_dose_volume_over_time_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + template_ = await cg.templatable(config[CONF_VOLUME], args, cg.double) + cg.add(var.set_volume(template_)) + + template_ = await cg.templatable(config[CONF_DURATION], args, int) + cg.add(var.set_duration(template_)) + + return var + + +EZO_PMP_DOSE_WITH_CONSTANT_FLOW_RATE_ACTION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + cv.Required(CONF_VOLUME_PER_MINUTE): cv.templatable( + cv.float_range() + ), # Any way to represent as proper volume (vs. raw int) + cv.Required(CONF_DURATION): cv.templatable( + cv.int_range(1) + ), # Any way to represent it as minutes (vs. raw int) + } +) + + +@automation.register_action( + "ezo_pmp.dose_with_constant_flow_rate", + EzoPMPDoseWithConstantFlowRateAction, + EZO_PMP_DOSE_WITH_CONSTANT_FLOW_RATE_ACTION_SCHEMA, +) +async def ezo_pmp_dose_with_constant_flow_rate_to_code( + config, action_id, template_arg, args +): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + template_ = await cg.templatable(config[CONF_VOLUME_PER_MINUTE], args, cg.double) + cg.add(var.set_volume(template_)) + + template_ = await cg.templatable(config[CONF_DURATION], args, int) + cg.add(var.set_duration(template_)) + + return var + + +EZO_PMP_SET_CALIBRATION_VOLUME_ACTION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + cv.Required(CONF_VOLUME): cv.templatable( + cv.float_range() + ), # Any way to represent as proper volume (vs. raw int) + } +) + + +@automation.register_action( + "ezo_pmp.set_calibration_volume", + EzoPMPSetCalibrationVolumeAction, + EZO_PMP_SET_CALIBRATION_VOLUME_ACTION_SCHEMA, +) +async def ezo_pmp_set_calibration_volume_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + template_ = await cg.templatable(config[CONF_VOLUME], args, cg.double) + cg.add(var.set_volume(template_)) + + return var + + +EZO_PMP_CHANGE_I2C_ADDRESS_ACTION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + cv.Required(CONF_ADDRESS): cv.templatable(cv.int_range(min=1, max=127)), + } +) + + +@automation.register_action( + "ezo_pmp.change_i2c_address", + EzoPMPChangeI2CAddressAction, + EZO_PMP_CHANGE_I2C_ADDRESS_ACTION_SCHEMA, +) +async def ezo_pmp_change_i2c_address_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.double) + cg.add(var.set_address(template_)) + + return var + + +EZO_PMP_ARBITRARY_COMMAND_ACTION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(EzoPMP), + cv.Required(CONF_COMMAND): cv.templatable(cv.string_strict), + } +) + + +@automation.register_action( + "ezo_pmp.arbitrary_command", + EzoPMPArbitraryCommandAction, + EZO_PMP_ARBITRARY_COMMAND_ACTION_SCHEMA, +) +async def ezo_pmp_arbitrary_command_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + template_ = await cg.templatable(config[CONF_COMMAND], args, cg.std_string) + cg.add(var.set_command(template_)) + + return var diff --git a/esphome/components/ezo_pmp/binary_sensor.py b/esphome/components/ezo_pmp/binary_sensor.py new file mode 100644 index 0000000000..582eb7af25 --- /dev/null +++ b/esphome/components/ezo_pmp/binary_sensor.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + ENTITY_CATEGORY_NONE, + DEVICE_CLASS_RUNNING, + DEVICE_CLASS_EMPTY, + CONF_ID, +) + +from . import EzoPMP + +DEPENDENCIES = ["ezo_pmp"] + +CONF_PUMP_STATE = "pump_state" +CONF_IS_PAUSED = "is_paused" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(EzoPMP), + cv.Optional(CONF_PUMP_STATE): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_RUNNING, + entity_category=ENTITY_CATEGORY_NONE, + ), + cv.Optional(CONF_IS_PAUSED): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_EMPTY, + entity_category=ENTITY_CATEGORY_NONE, + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + + if CONF_PUMP_STATE in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_PUMP_STATE]) + cg.add(parent.set_is_dosing(sens)) + + if CONF_IS_PAUSED in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_IS_PAUSED]) + cg.add(parent.set_is_paused(sens)) diff --git a/esphome/components/ezo_pmp/ezo_pmp.cpp b/esphome/components/ezo_pmp/ezo_pmp.cpp new file mode 100644 index 0000000000..6e5779a12e --- /dev/null +++ b/esphome/components/ezo_pmp/ezo_pmp.cpp @@ -0,0 +1,543 @@ +#include "ezo_pmp.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace ezo_pmp { + +static const char *const TAG = "ezo-pmp"; + +static const uint16_t EZO_PMP_COMMAND_NONE = 0; +static const uint16_t EZO_PMP_COMMAND_TYPE_READ = 1; + +static const uint16_t EZO_PMP_COMMAND_FIND = 2; +static const uint16_t EZO_PMP_COMMAND_DOSE_CONTINUOUSLY = 4; +static const uint16_t EZO_PMP_COMMAND_DOSE_VOLUME = 8; +static const uint16_t EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME = 16; +static const uint16_t EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE = 32; +static const uint16_t EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME = 64; +static const uint16_t EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED = 128; +static const uint16_t EZO_PMP_COMMAND_CLEAR_CALIBRATION = 256; +static const uint16_t EZO_PMP_COMMAND_PAUSE_DOSING = 512; +static const uint16_t EZO_PMP_COMMAND_STOP_DOSING = 1024; +static const uint16_t EZO_PMP_COMMAND_CHANGE_I2C_ADDRESS = 2048; +static const uint16_t EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS = 4096; + +static const uint16_t EZO_PMP_COMMAND_READ_DOSING = 3; +static const uint16_t EZO_PMP_COMMAND_READ_SINGLE_REPORT = 5; +static const uint16_t EZO_PMP_COMMAND_READ_MAX_FLOW_RATE = 9; +static const uint16_t EZO_PMP_COMMAND_READ_PAUSE_STATUS = 17; +static const uint16_t EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED = 33; +static const uint16_t EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED = 65; +static const uint16_t EZO_PMP_COMMAND_READ_CALIBRATION_STATUS = 129; +static const uint16_t EZO_PMP_COMMAND_READ_PUMP_VOLTAGE = 257; + +static const std::string DOSING_MODE_NONE = "None"; +static const std::string DOSING_MODE_VOLUME = "Volume"; +static const std::string DOSING_MODE_VOLUME_OVER_TIME = "Volume/Time"; +static const std::string DOSING_MODE_CONSTANT_FLOW_RATE = "Constant Flow Rate"; +static const std::string DOSING_MODE_CONTINUOUS = "Continuous"; + +void EzoPMP::dump_config() { + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with EZO-PMP circuit failed!"); + } + LOG_UPDATE_INTERVAL(this); +} + +void EzoPMP::update() { + if (this->is_waiting_) { + return; + } + + if (this->is_first_read_) { + this->queue_command_(EZO_PMP_COMMAND_READ_CALIBRATION_STATUS, 0, 0, (bool) this->calibration_status_); + this->queue_command_(EZO_PMP_COMMAND_READ_MAX_FLOW_RATE, 0, 0, (bool) this->max_flow_rate_); + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_); + this->queue_command_(EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED, 0, 0, (bool) this->total_volume_dosed_); + this->queue_command_(EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED, 0, 0, + (bool) this->absolute_total_volume_dosed_); + this->queue_command_(EZO_PMP_COMMAND_READ_PAUSE_STATUS, 0, 0, true); + this->is_first_read_ = false; + } + + if (!this->is_waiting_ && this->peek_next_command_() == EZO_PMP_COMMAND_NONE) { + this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true); + + if (this->is_dosing_flag_) { + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_); + this->queue_command_(EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED, 0, 0, (bool) this->total_volume_dosed_); + this->queue_command_(EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED, 0, 0, + (bool) this->absolute_total_volume_dosed_); + } + + this->queue_command_(EZO_PMP_COMMAND_READ_PUMP_VOLTAGE, 0, 0, (bool) this->pump_voltage_); + } else { + ESP_LOGV(TAG, "Not Scheduling new Command during update()"); + } +} + +void EzoPMP::loop() { + // If we are not waiting for anything and there is no command to be sent, return + if (!this->is_waiting_ && this->peek_next_command_() == EZO_PMP_COMMAND_NONE) { + return; + } + + // If we are not waiting for anything and there IS a command to be sent, do it. + if (!this->is_waiting_ && this->peek_next_command_() != EZO_PMP_COMMAND_NONE) { + this->send_next_command_(); + } + + // If we are waiting for something but it isn't ready yet, then return + if (this->is_waiting_ && millis() - this->start_time_ < this->wait_time_) { + return; + } + + // We are waiting for something and it should be ready. + this->read_command_result_(); +} + +void EzoPMP::clear_current_command_() { + this->current_command_ = EZO_PMP_COMMAND_NONE; + this->is_waiting_ = false; +} + +void EzoPMP::read_command_result_() { + uint8_t response_buffer[21] = {'\0'}; + + response_buffer[0] = 0; + if (!this->read_bytes_raw(response_buffer, 20)) { + ESP_LOGE(TAG, "read error"); + this->clear_current_command_(); + return; + } + + switch (response_buffer[0]) { + case 254: + return; // keep waiting + case 1: + break; + case 2: + ESP_LOGE(TAG, "device returned a syntax error"); + this->clear_current_command_(); + return; + case 255: + ESP_LOGE(TAG, "device returned no data"); + this->clear_current_command_(); + return; + default: + ESP_LOGE(TAG, "device returned an unknown response: %d", response_buffer[0]); + this->clear_current_command_(); + return; + } + + char first_parameter_buffer[10] = {'\0'}; + char second_parameter_buffer[10] = {'\0'}; + char third_parameter_buffer[10] = {'\0'}; + + first_parameter_buffer[0] = '\0'; + second_parameter_buffer[0] = '\0'; + third_parameter_buffer[0] = '\0'; + + int current_parameter = 1; + + size_t position_in_parameter_buffer = 0; + // some sensors return multiple comma-separated values, terminate string after first one + for (size_t i = 1; i < sizeof(response_buffer) - 1; i++) { + char current_char = response_buffer[i]; + + if (current_char == '\0') { + ESP_LOGV(TAG, "Read Response from device: %s", (char *) response_buffer); + ESP_LOGV(TAG, "First Component: %s", (char *) first_parameter_buffer); + ESP_LOGV(TAG, "Second Component: %s", (char *) second_parameter_buffer); + ESP_LOGV(TAG, "Third Component: %s", (char *) third_parameter_buffer); + + break; + } + + if (current_char == ',') { + current_parameter++; + position_in_parameter_buffer = 0; + continue; + } + + switch (current_parameter) { + case 1: + first_parameter_buffer[position_in_parameter_buffer] = current_char; + first_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; + break; + case 2: + second_parameter_buffer[position_in_parameter_buffer] = current_char; + second_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; + break; + case 3: + third_parameter_buffer[position_in_parameter_buffer] = current_char; + third_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; + break; + } + + position_in_parameter_buffer++; + } + + auto parsed_first_parameter = parse_number(first_parameter_buffer); + auto parsed_second_parameter = parse_number(second_parameter_buffer); + auto parsed_third_parameter = parse_number(third_parameter_buffer); + + switch (this->current_command_) { + // Read Commands + case EZO_PMP_COMMAND_READ_DOSING: // Page 54 + if (parsed_third_parameter.has_value()) + this->is_dosing_flag_ = parsed_third_parameter.value_or(0) == 1; + + if (this->is_dosing_) + this->is_dosing_->publish_state(this->is_dosing_flag_); + + if (parsed_second_parameter.has_value() && this->last_volume_requested_) { + this->last_volume_requested_->publish_state(parsed_second_parameter.value_or(0)); + } + + if (!this->is_dosing_flag_ && !this->is_paused_flag_) { + // If pump is not paused and not dispensing + if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_NONE) + this->dosing_mode_->publish_state(DOSING_MODE_NONE); + } + + break; + + case EZO_PMP_COMMAND_READ_SINGLE_REPORT: // Single Report (page 53) + if (parsed_first_parameter.has_value() && (bool) this->current_volume_dosed_) { + this->current_volume_dosed_->publish_state(parsed_first_parameter.value_or(0)); + } + break; + + case EZO_PMP_COMMAND_READ_MAX_FLOW_RATE: // Constant Flow Rate (page 57) + if (parsed_second_parameter.has_value() && this->max_flow_rate_) + this->max_flow_rate_->publish_state(parsed_second_parameter.value_or(0)); + break; + + case EZO_PMP_COMMAND_READ_PAUSE_STATUS: // Pause (page 61) + if (parsed_second_parameter.has_value()) + this->is_paused_flag_ = parsed_second_parameter.value_or(0) == 1; + + if (this->is_paused_) + this->is_paused_->publish_state(this->is_paused_flag_); + break; + + case EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED: // Total Volume Dispensed (page 64) + if (parsed_second_parameter.has_value() && this->total_volume_dosed_) + this->total_volume_dosed_->publish_state(parsed_second_parameter.value_or(0)); + break; + + case EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED: // Total Volume Dispensed (page 64) + if (parsed_second_parameter.has_value() && this->absolute_total_volume_dosed_) + this->absolute_total_volume_dosed_->publish_state(parsed_second_parameter.value_or(0)); + break; + + case EZO_PMP_COMMAND_READ_CALIBRATION_STATUS: // Calibration (page 65) + if (parsed_second_parameter.has_value() && this->calibration_status_) { + if (parsed_second_parameter.value_or(0) == 1) { + this->calibration_status_->publish_state("Fixed Volume"); + } else if (parsed_second_parameter.value_or(0) == 2) { + this->calibration_status_->publish_state("Volume/Time"); + } else if (parsed_second_parameter.value_or(0) == 3) { + this->calibration_status_->publish_state("Fixed Volume & Volume/Time"); + } else { + this->calibration_status_->publish_state("Uncalibrated"); + } + } + break; + + case EZO_PMP_COMMAND_READ_PUMP_VOLTAGE: // Pump Voltage (page 67) + if (parsed_second_parameter.has_value() && this->pump_voltage_) + this->pump_voltage_->publish_state(parsed_second_parameter.value_or(0)); + break; + + // Non-Read Commands + + case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55) + if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_VOLUME) + this->dosing_mode_->publish_state(DOSING_MODE_VOLUME); + break; + + case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56) + if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_VOLUME_OVER_TIME) + this->dosing_mode_->publish_state(DOSING_MODE_VOLUME_OVER_TIME); + break; + + case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57) + if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_CONSTANT_FLOW_RATE) + this->dosing_mode_->publish_state(DOSING_MODE_CONSTANT_FLOW_RATE); + break; + + case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54) + if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_CONTINUOUS) + this->dosing_mode_->publish_state(DOSING_MODE_CONTINUOUS); + break; + + case EZO_PMP_COMMAND_STOP_DOSING: // Stop (page 62) + this->is_paused_flag_ = false; + if (this->is_paused_) + this->is_paused_->publish_state(this->is_paused_flag_); + if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_NONE) + this->dosing_mode_->publish_state(DOSING_MODE_NONE); + break; + + case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS: + ESP_LOGI(TAG, "Arbitrary Command Response: %s", (char *) response_buffer); + break; + + case EZO_PMP_COMMAND_CLEAR_CALIBRATION: // Clear Calibration (page 65) + case EZO_PMP_COMMAND_PAUSE_DOSING: // Pause (page 61) + case EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME: // Set Calibration Volume (page 65) + case EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED: // Clear Total Volume Dosed (page 64) + case EZO_PMP_COMMAND_FIND: // Find (page 52) + // Nothing to do here + break; + + case EZO_PMP_COMMAND_TYPE_READ: + case EZO_PMP_COMMAND_NONE: + default: + ESP_LOGE(TAG, "Unsupported command received: %d", this->current_command_); + return; + } + + this->clear_current_command_(); +} + +void EzoPMP::send_next_command_() { + int wait_time_for_command = 400; // milliseconds + uint8_t command_buffer[21]; + int command_buffer_length = 0; + + this->pop_next_command_(); // this->next_command will be updated. + + switch (this->next_command_) { + // Read Commands + case EZO_PMP_COMMAND_READ_DOSING: // Page 54 + command_buffer_length = sprintf((char *) command_buffer, "D,?"); + break; + + case EZO_PMP_COMMAND_READ_SINGLE_REPORT: // Single Report (page 53) + command_buffer_length = sprintf((char *) command_buffer, "R"); + break; + + case EZO_PMP_COMMAND_READ_MAX_FLOW_RATE: + command_buffer_length = sprintf((char *) command_buffer, "DC,?"); + break; + + case EZO_PMP_COMMAND_READ_PAUSE_STATUS: + command_buffer_length = sprintf((char *) command_buffer, "P,?"); + break; + + case EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED: + command_buffer_length = sprintf((char *) command_buffer, "TV,?"); + break; + + case EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED: + command_buffer_length = sprintf((char *) command_buffer, "ATV,?"); + break; + + case EZO_PMP_COMMAND_READ_CALIBRATION_STATUS: + command_buffer_length = sprintf((char *) command_buffer, "Cal,?"); + break; + + case EZO_PMP_COMMAND_READ_PUMP_VOLTAGE: + command_buffer_length = sprintf((char *) command_buffer, "PV,?"); + break; + + // Non-Read Commands + + case EZO_PMP_COMMAND_FIND: // Find (page 52) + command_buffer_length = sprintf((char *) command_buffer, "Find"); + wait_time_for_command = 60000; // This command will block all updates for a minute + break; + + case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54) + command_buffer_length = sprintf((char *) command_buffer, "D,*"); + break; + + case EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED: // Clear Total Volume Dosed (page 64) + command_buffer_length = sprintf((char *) command_buffer, "Clear"); + break; + + case EZO_PMP_COMMAND_CLEAR_CALIBRATION: // Clear Calibration (page 65) + command_buffer_length = sprintf((char *) command_buffer, "Cal,clear"); + break; + + case EZO_PMP_COMMAND_PAUSE_DOSING: // Pause (page 61) + command_buffer_length = sprintf((char *) command_buffer, "P"); + break; + + case EZO_PMP_COMMAND_STOP_DOSING: // Stop (page 62) + command_buffer_length = sprintf((char *) command_buffer, "X"); + break; + + // Non-Read commands with parameters + + case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55) + command_buffer_length = sprintf((char *) command_buffer, "D,%0.1f", this->next_command_volume_); + break; + + case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56) + command_buffer_length = + sprintf((char *) command_buffer, "D,%0.1f,%i", this->next_command_volume_, this->next_command_duration_); + break; + + case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57) + command_buffer_length = + sprintf((char *) command_buffer, "DC,%0.1f,%i", this->next_command_volume_, this->next_command_duration_); + break; + + case EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME: // Set Calibration Volume (page 65) + command_buffer_length = sprintf((char *) command_buffer, "Cal,%0.2f", this->next_command_volume_); + break; + + case EZO_PMP_COMMAND_CHANGE_I2C_ADDRESS: // Change I2C Address (page 73) + command_buffer_length = sprintf((char *) command_buffer, "I2C,%i", this->next_command_duration_); + break; + + case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS: // Run an arbitrary command + command_buffer_length = sprintf((char *) command_buffer, this->arbitrary_command_, this->next_command_duration_); + ESP_LOGI(TAG, "Sending arbitrary command: %s", (char *) command_buffer); + break; + + case EZO_PMP_COMMAND_TYPE_READ: + case EZO_PMP_COMMAND_NONE: + default: + ESP_LOGE(TAG, "Unsupported command received: %d", this->next_command_); + return; + } + + // Send command + ESP_LOGV(TAG, "Sending command to device: %s", (char *) command_buffer); + this->write(command_buffer, command_buffer_length); + + this->current_command_ = this->next_command_; + this->next_command_ = EZO_PMP_COMMAND_NONE; + this->is_waiting_ = true; + this->start_time_ = millis(); + this->wait_time_ = wait_time_for_command; +} + +void EzoPMP::pop_next_command_() { + if (this->next_command_queue_length_ <= 0) { + ESP_LOGE(TAG, "Tried to dequeue command from empty queue"); + this->next_command_ = EZO_PMP_COMMAND_NONE; + this->next_command_volume_ = 0; + this->next_command_duration_ = 0; + return; + } + + // Read from Head + this->next_command_ = this->next_command_queue_[this->next_command_queue_head_]; + this->next_command_volume_ = this->next_command_volume_queue_[this->next_command_queue_head_]; + this->next_command_duration_ = this->next_command_duration_queue_[this->next_command_queue_head_]; + + // Move positions + next_command_queue_head_++; + if (next_command_queue_head_ >= 10) { + next_command_queue_head_ = 0; + } + + next_command_queue_length_--; +} + +uint16_t EzoPMP::peek_next_command_() { + if (this->next_command_queue_length_ <= 0) { + return EZO_PMP_COMMAND_NONE; + } + + return this->next_command_queue_[this->next_command_queue_head_]; +} + +void EzoPMP::queue_command_(uint16_t command, double volume, int duration, bool should_schedule) { + if (!should_schedule) { + return; + } + + if (this->next_command_queue_length_ >= 10) { + ESP_LOGE(TAG, "Tried to queue command '%d' but queue is full", command); + return; + } + + this->next_command_queue_[this->next_command_queue_last_] = command; + this->next_command_volume_queue_[this->next_command_queue_last_] = volume; + this->next_command_duration_queue_[this->next_command_queue_last_] = duration; + + ESP_LOGV(TAG, "Queue command '%d' in position '%d'", command, next_command_queue_last_); + + // Move positions + next_command_queue_last_++; + if (next_command_queue_last_ >= 10) { + next_command_queue_last_ = 0; + } + + next_command_queue_length_++; +} + +// Actions + +void EzoPMP::find() { this->queue_command_(EZO_PMP_COMMAND_FIND, 0, 0, true); } + +void EzoPMP::dose_continuously() { + this->queue_command_(EZO_PMP_COMMAND_DOSE_CONTINUOUSLY, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_); +} + +void EzoPMP::dose_volume(double volume) { + this->queue_command_(EZO_PMP_COMMAND_DOSE_VOLUME, volume, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_); +} + +void EzoPMP::dose_volume_over_time(double volume, int duration) { + this->queue_command_(EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME, volume, duration, true); + this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_); +} + +void EzoPMP::dose_with_constant_flow_rate(double volume, int duration) { + this->queue_command_(EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE, volume, duration, true); + this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_); +} + +void EzoPMP::set_calibration_volume(double volume) { + this->queue_command_(EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME, volume, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_CALIBRATION_STATUS, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_MAX_FLOW_RATE, 0, 0, true); +} + +void EzoPMP::clear_total_volume_dosed() { + this->queue_command_(EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED, 0, 0, true); +} + +void EzoPMP::clear_calibration() { + this->queue_command_(EZO_PMP_COMMAND_CLEAR_CALIBRATION, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_CALIBRATION_STATUS, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_MAX_FLOW_RATE, 0, 0, true); +} + +void EzoPMP::pause_dosing() { + this->queue_command_(EZO_PMP_COMMAND_PAUSE_DOSING, 0, 0, true); + this->queue_command_(EZO_PMP_COMMAND_READ_PAUSE_STATUS, 0, 0, true); +} + +void EzoPMP::stop_dosing() { this->queue_command_(EZO_PMP_COMMAND_STOP_DOSING, 0, 0, true); } + +void EzoPMP::change_i2c_address(int address) { + this->queue_command_(EZO_PMP_COMMAND_CHANGE_I2C_ADDRESS, 0, address, true); +} + +void EzoPMP::exec_arbitrary_command(const std::basic_string &command) { + this->arbitrary_command_ = command.c_str(); + this->queue_command_(EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS, 0, 0, true); +} + +} // namespace ezo_pmp +} // namespace esphome diff --git a/esphome/components/ezo_pmp/ezo_pmp.h b/esphome/components/ezo_pmp/ezo_pmp.h new file mode 100644 index 0000000000..b41710cd78 --- /dev/null +++ b/esphome/components/ezo_pmp/ezo_pmp.h @@ -0,0 +1,252 @@ +#pragma once + +#include "esphome/core/defines.h" +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/i2c/i2c.h" + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif + +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif + +namespace esphome { +namespace ezo_pmp { + +class EzoPMP : public PollingComponent, public i2c::I2CDevice { + public: + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; }; + + void loop() override; + void update() override; + +#ifdef USE_SENSOR + void set_current_volume_dosed(sensor::Sensor *current_volume_dosed) { current_volume_dosed_ = current_volume_dosed; } + void set_total_volume_dosed(sensor::Sensor *total_volume_dosed) { total_volume_dosed_ = total_volume_dosed; } + void set_absolute_total_volume_dosed(sensor::Sensor *absolute_total_volume_dosed) { + absolute_total_volume_dosed_ = absolute_total_volume_dosed; + } + void set_pump_voltage(sensor::Sensor *pump_voltage) { pump_voltage_ = pump_voltage; } + void set_last_volume_requested(sensor::Sensor *last_volume_requested) { + last_volume_requested_ = last_volume_requested; + } + void set_max_flow_rate(sensor::Sensor *max_flow_rate) { max_flow_rate_ = max_flow_rate; } +#endif + +#ifdef USE_BINARY_SENSOR + void set_is_dosing(binary_sensor::BinarySensor *is_dosing) { is_dosing_ = is_dosing; } + void set_is_paused(binary_sensor::BinarySensor *is_paused) { is_paused_ = is_paused; } +#endif + +#ifdef USE_TEXT_SENSOR + void set_dosing_mode(text_sensor::TextSensor *dosing_mode) { dosing_mode_ = dosing_mode; } + void set_calibration_status(text_sensor::TextSensor *calibration_status) { calibration_status_ = calibration_status; } +#endif + + // Actions for EZO-PMP + void find(); + void dose_continuously(); + void dose_volume(double volume); + void dose_volume_over_time(double volume, int duration); + void dose_with_constant_flow_rate(double volume, int duration); + void set_calibration_volume(double volume); + void clear_total_volume_dosed(); + void clear_calibration(); + void pause_dosing(); + void stop_dosing(); + void change_i2c_address(int address); + void exec_arbitrary_command(const std::basic_string &command); + + protected: + uint32_t start_time_ = 0; + uint32_t wait_time_ = 0; + bool is_waiting_ = false; + bool is_first_read_ = true; + + uint16_t next_command_ = 0; + double next_command_volume_ = 0; // might be negative + int next_command_duration_ = 0; + + uint16_t next_command_queue_[10]; + double next_command_volume_queue_[10]; + int next_command_duration_queue_[10]; + int next_command_queue_head_ = 0; + int next_command_queue_last_ = 0; + int next_command_queue_length_ = 0; + + uint16_t current_command_ = 0; + bool is_paused_flag_ = false; + bool is_dosing_flag_ = false; + + const char *arbitrary_command_{nullptr}; + + void send_next_command_(); + void read_command_result_(); + void clear_current_command_(); + void queue_command_(uint16_t command, double volume, int duration, bool should_schedule); + void pop_next_command_(); + uint16_t peek_next_command_(); + +#ifdef USE_SENSOR + sensor::Sensor *current_volume_dosed_{nullptr}; + sensor::Sensor *total_volume_dosed_{nullptr}; + sensor::Sensor *absolute_total_volume_dosed_{nullptr}; + sensor::Sensor *pump_voltage_{nullptr}; + sensor::Sensor *max_flow_rate_{nullptr}; + sensor::Sensor *last_volume_requested_{nullptr}; +#endif + +#ifdef USE_BINARY_SENSOR + binary_sensor::BinarySensor *is_dosing_{nullptr}; + binary_sensor::BinarySensor *is_paused_{nullptr}; +#endif + +#ifdef USE_TEXT_SENSOR + text_sensor::TextSensor *dosing_mode_{nullptr}; + text_sensor::TextSensor *calibration_status_{nullptr}; +#endif +}; + +// Action Templates +template class EzoPMPFindAction : public Action { + public: + EzoPMPFindAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->find(); } + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPDoseContinuouslyAction : public Action { + public: + EzoPMPDoseContinuouslyAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->dose_continuously(); } + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPDoseVolumeAction : public Action { + public: + EzoPMPDoseVolumeAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->dose_volume(this->volume_.value(x...)); } + TEMPLATABLE_VALUE(double, volume) + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPDoseVolumeOverTimeAction : public Action { + public: + EzoPMPDoseVolumeOverTimeAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { + this->ezopmp_->dose_volume_over_time(this->volume_.value(x...), this->duration_.value(x...)); + } + TEMPLATABLE_VALUE(double, volume) + TEMPLATABLE_VALUE(int, duration) + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPDoseWithConstantFlowRateAction : public Action { + public: + EzoPMPDoseWithConstantFlowRateAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { + this->ezopmp_->dose_with_constant_flow_rate(this->volume_.value(x...), this->duration_.value(x...)); + } + TEMPLATABLE_VALUE(double, volume) + TEMPLATABLE_VALUE(int, duration) + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPSetCalibrationVolumeAction : public Action { + public: + EzoPMPSetCalibrationVolumeAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->set_calibration_volume(this->volume_.value(x...)); } + TEMPLATABLE_VALUE(double, volume) + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPClearTotalVolumeDispensedAction : public Action { + public: + EzoPMPClearTotalVolumeDispensedAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->clear_total_volume_dosed(); } + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPClearCalibrationAction : public Action { + public: + EzoPMPClearCalibrationAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->clear_calibration(); } + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPPauseDosingAction : public Action { + public: + EzoPMPPauseDosingAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->pause_dosing(); } + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPStopDosingAction : public Action { + public: + EzoPMPStopDosingAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->stop_dosing(); } + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPChangeI2CAddressAction : public Action { + public: + EzoPMPChangeI2CAddressAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->change_i2c_address(this->address_.value(x...)); } + TEMPLATABLE_VALUE(int, address) + + protected: + EzoPMP *ezopmp_; +}; + +template class EzoPMPArbitraryCommandAction : public Action { + public: + EzoPMPArbitraryCommandAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} + + void play(Ts... x) override { this->ezopmp_->exec_arbitrary_command(this->command_.value(x...)); } + TEMPLATABLE_VALUE(std::string, command) + + protected: + EzoPMP *ezopmp_; +}; + +} // namespace ezo_pmp +} // namespace esphome diff --git a/esphome/components/ezo_pmp/sensor.py b/esphome/components/ezo_pmp/sensor.py new file mode 100644 index 0000000000..737985f4c5 --- /dev/null +++ b/esphome/components/ezo_pmp/sensor.py @@ -0,0 +1,104 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + ENTITY_CATEGORY_DIAGNOSTIC, + ENTITY_CATEGORY_NONE, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_NONE, + CONF_ID, + UNIT_VOLT, +) + +from . import EzoPMP + + +DEPENDENCIES = ["ezo_pmp"] + +CONF_CURRENT_VOLUME_DOSED = "current_volume_dosed" +CONF_TOTAL_VOLUME_DOSED = "total_volume_dosed" +CONF_ABSOLUTE_TOTAL_VOLUME_DOSED = "absolute_total_volume_dosed" +CONF_PUMP_VOLTAGE = "pump_voltage" +CONF_LAST_VOLUME_REQUESTED = "last_volume_requested" +CONF_MAX_FLOW_RATE = "max_flow_rate" + +UNIT_MILILITER = "ml" +UNIT_MILILITERS_PER_MINUTE = "ml/min" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(EzoPMP), + cv.Optional(CONF_CURRENT_VOLUME_DOSED): sensor.sensor_schema( + unit_of_measurement=UNIT_MILILITER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_NONE, + ), + cv.Optional(CONF_LAST_VOLUME_REQUESTED): sensor.sensor_schema( + unit_of_measurement=UNIT_MILILITER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_NONE, + ), + cv.Optional(CONF_MAX_FLOW_RATE): sensor.sensor_schema( + unit_of_measurement=UNIT_MILILITERS_PER_MINUTE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_NONE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_TOTAL_VOLUME_DOSED): sensor.sensor_schema( + unit_of_measurement=UNIT_MILILITER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_ABSOLUTE_TOTAL_VOLUME_DOSED): sensor.sensor_schema( + unit_of_measurement=UNIT_MILILITER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_PUMP_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + + if CONF_CURRENT_VOLUME_DOSED in config: + sens = await sensor.new_sensor(config[CONF_CURRENT_VOLUME_DOSED]) + cg.add(parent.set_current_volume_dosed(sens)) + + if CONF_LAST_VOLUME_REQUESTED in config: + sens = await sensor.new_sensor(config[CONF_LAST_VOLUME_REQUESTED]) + cg.add(parent.set_last_volume_requested(sens)) + + if CONF_TOTAL_VOLUME_DOSED in config: + sens = await sensor.new_sensor(config[CONF_TOTAL_VOLUME_DOSED]) + cg.add(parent.set_total_volume_dosed(sens)) + + if CONF_ABSOLUTE_TOTAL_VOLUME_DOSED in config: + sens = await sensor.new_sensor(config[CONF_ABSOLUTE_TOTAL_VOLUME_DOSED]) + cg.add(parent.set_absolute_total_volume_dosed(sens)) + + if CONF_PUMP_VOLTAGE in config: + sens = await sensor.new_sensor(config[CONF_PUMP_VOLTAGE]) + cg.add(parent.set_pump_voltage(sens)) + + if CONF_MAX_FLOW_RATE in config: + sens = await sensor.new_sensor(config[CONF_MAX_FLOW_RATE]) + cg.add(parent.set_max_flow_rate(sens)) diff --git a/esphome/components/ezo_pmp/text_sensor.py b/esphome/components/ezo_pmp/text_sensor.py new file mode 100644 index 0000000000..f8f133e316 --- /dev/null +++ b/esphome/components/ezo_pmp/text_sensor.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import ( + ENTITY_CATEGORY_NONE, + ENTITY_CATEGORY_DIAGNOSTIC, + CONF_ID, +) + +from . import EzoPMP + +DEPENDENCIES = ["ezo_pmp"] + +CONF_DOSING_MODE = "dosing_mode" +CONF_CALIBRATION_STATUS = "calibration_status" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(EzoPMP), + cv.Optional(CONF_DOSING_MODE): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_NONE, + ), + cv.Optional(CONF_CALIBRATION_STATUS): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + + if CONF_DOSING_MODE in config: + sens = await text_sensor.new_text_sensor(config[CONF_DOSING_MODE]) + cg.add(parent.set_dosing_mode(sens)) + + if CONF_CALIBRATION_STATUS in config: + sens = await text_sensor.new_text_sensor(config[CONF_CALIBRATION_STATUS]) + cg.add(parent.set_calibration_status(sens)) diff --git a/esphome/components/factory_reset/__init__.py b/esphome/components/factory_reset/__init__.py new file mode 100644 index 0000000000..f1bcfd8c55 --- /dev/null +++ b/esphome/components/factory_reset/__init__.py @@ -0,0 +1,5 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@anatoly-savchenkov"] + +factory_reset_ns = cg.esphome_ns.namespace("factory_reset") diff --git a/esphome/components/factory_reset/button/__init__.py b/esphome/components/factory_reset/button/__init__.py new file mode 100644 index 0000000000..010691ac7f --- /dev/null +++ b/esphome/components/factory_reset/button/__init__.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import button +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_RESTART, + ENTITY_CATEGORY_CONFIG, + ICON_RESTART_ALERT, +) +from .. import factory_reset_ns + +FactoryResetButton = factory_reset_ns.class_( + "FactoryResetButton", button.Button, cg.Component +) + +CONFIG_SCHEMA = button.button_schema( + FactoryResetButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART_ALERT, +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await button.register_button(var, config) diff --git a/esphome/components/factory_reset/button/factory_reset_button.cpp b/esphome/components/factory_reset/button/factory_reset_button.cpp new file mode 100644 index 0000000000..9354a3363e --- /dev/null +++ b/esphome/components/factory_reset/button/factory_reset_button.cpp @@ -0,0 +1,21 @@ +#include "factory_reset_button.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace factory_reset { + +static const char *const TAG = "factory_reset.button"; + +void FactoryResetButton::dump_config() { LOG_BUTTON("", "Factory Reset Button", this); } +void FactoryResetButton::press_action() { + ESP_LOGI(TAG, "Resetting to factory defaults..."); + // Let MQTT settle a bit + delay(100); // NOLINT + global_preferences->reset(); + App.safe_reboot(); +} + +} // namespace factory_reset +} // namespace esphome diff --git a/esphome/components/factory_reset/button/factory_reset_button.h b/esphome/components/factory_reset/button/factory_reset_button.h new file mode 100644 index 0000000000..9996a860d9 --- /dev/null +++ b/esphome/components/factory_reset/button/factory_reset_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/button/button.h" + +namespace esphome { +namespace factory_reset { + +class FactoryResetButton : public button::Button, public Component { + public: + void dump_config() override; + + protected: + void press_action() override; +}; + +} // namespace factory_reset +} // namespace esphome diff --git a/esphome/components/factory_reset/switch/__init__.py b/esphome/components/factory_reset/switch/__init__.py new file mode 100644 index 0000000000..3cc19a35a3 --- /dev/null +++ b/esphome/components/factory_reset/switch/__init__.py @@ -0,0 +1,35 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ID, + CONF_INVERTED, + CONF_ICON, + ENTITY_CATEGORY_CONFIG, + ICON_RESTART_ALERT, +) +from .. import factory_reset_ns + +FactoryResetSwitch = factory_reset_ns.class_( + "FactoryResetSwitch", switch.Switch, cg.Component +) + +CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(FactoryResetSwitch), + cv.Optional(CONF_INVERTED): cv.invalid( + "Factory Reset switches do not support inverted mode!" + ), + cv.Optional(CONF_ICON, default=ICON_RESTART_ALERT): cv.icon, + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG + ): cv.entity_category, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await switch.register_switch(var, config) diff --git a/esphome/components/factory_reset/switch/factory_reset_switch.cpp b/esphome/components/factory_reset/switch/factory_reset_switch.cpp new file mode 100644 index 0000000000..7bc8676736 --- /dev/null +++ b/esphome/components/factory_reset/switch/factory_reset_switch.cpp @@ -0,0 +1,26 @@ +#include "factory_reset_switch.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace factory_reset { + +static const char *const TAG = "factory_reset.switch"; + +void FactoryResetSwitch::dump_config() { LOG_SWITCH("", "Factory Reset Switch", this); } +void FactoryResetSwitch::write_state(bool state) { + // Acknowledge + this->publish_state(false); + + if (state) { + ESP_LOGI(TAG, "Resetting to factory defaults..."); + // Let MQTT settle a bit + delay(100); // NOLINT + global_preferences->reset(); + App.safe_reboot(); + } +} + +} // namespace factory_reset +} // namespace esphome diff --git a/esphome/components/factory_reset/switch/factory_reset_switch.h b/esphome/components/factory_reset/switch/factory_reset_switch.h new file mode 100644 index 0000000000..2c914ea76d --- /dev/null +++ b/esphome/components/factory_reset/switch/factory_reset_switch.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace factory_reset { + +class FactoryResetSwitch : public switch_::Switch, public Component { + public: + void dump_config() override; + + protected: + void write_state(bool state) override; +}; + +} // namespace factory_reset +} // namespace esphome diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index eb67bbcbd7..9a05bff3a0 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -63,7 +63,7 @@ FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.temp FAN_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.GenerateID(): cv.declare_id(Fan), - cv.Optional(CONF_RESTORE_MODE, default="RESTORE_DEFAULT_OFF"): cv.enum( + cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum( RESTORE_MODES, upper=True, space="_" ), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTFanComponent), diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 5f9660f6d6..87566bad4a 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -1,5 +1,4 @@ #include "fan.h" -#include "fan_helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -21,14 +20,18 @@ const LogString *fan_direction_to_string(FanDirection direction) { void FanCall::perform() { ESP_LOGD(TAG, "'%s' - Setting:", this->parent_.get_name().c_str()); this->validate_(); - if (this->binary_state_.has_value()) + if (this->binary_state_.has_value()) { ESP_LOGD(TAG, " State: %s", ONOFF(*this->binary_state_)); - if (this->oscillating_.has_value()) + } + if (this->oscillating_.has_value()) { ESP_LOGD(TAG, " Oscillating: %s", YESNO(*this->oscillating_)); - if (this->speed_.has_value()) + } + if (this->speed_.has_value()) { ESP_LOGD(TAG, " Speed: %d", *this->speed_); - if (this->direction_.has_value()) + } + if (this->direction_.has_value()) { ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); + } this->parent_.control(*this); } @@ -61,22 +64,6 @@ void FanCall::validate_() { } } -// This whole method is deprecated, don't warn about usage of deprecated methods inside of it. -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" -FanCall &FanCall::set_speed(const char *legacy_speed) { - const auto supported_speed_count = this->parent_.get_traits().supported_speed_count(); - if (strcasecmp(legacy_speed, "low") == 0) { - this->set_speed(fan::speed_enum_to_level(FAN_SPEED_LOW, supported_speed_count)); - } else if (strcasecmp(legacy_speed, "medium") == 0) { - this->set_speed(fan::speed_enum_to_level(FAN_SPEED_MEDIUM, supported_speed_count)); - } else if (strcasecmp(legacy_speed, "high") == 0) { - this->set_speed(fan::speed_enum_to_level(FAN_SPEED_HIGH, supported_speed_count)); - } - return *this; -} -#pragma GCC diagnostic pop - FanCall FanRestoreState::to_call(Fan &fan) { auto call = fan.make_call(); call.set_state(this->state); @@ -93,9 +80,6 @@ void FanRestoreState::apply(Fan &fan) { fan.publish_state(); } -Fan::Fan() : EntityBase("") {} -Fan::Fan(const std::string &name) : EntityBase(name) {} - FanCall Fan::turn_on() { return this->make_call().set_state(true); } FanCall Fan::turn_off() { return this->make_call().set_state(false); } FanCall Fan::toggle() { return this->make_call().set_state(!this->state); } @@ -107,12 +91,15 @@ void Fan::publish_state() { ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str()); ESP_LOGD(TAG, " State: %s", ONOFF(this->state)); - if (traits.supports_speed()) + if (traits.supports_speed()) { ESP_LOGD(TAG, " Speed: %d", this->speed); - if (traits.supports_oscillation()) + } + if (traits.supports_oscillation()) { ESP_LOGD(TAG, " Oscillating: %s", YESNO(this->oscillating)); - if (traits.supports_direction()) + } + if (traits.supports_direction()) { ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); + } this->state_callback_.call(); this->save_state_(); @@ -164,12 +151,13 @@ void Fan::dump_traits_(const char *tag, const char *prefix) { ESP_LOGCONFIG(tag, "%s Speed: YES", prefix); ESP_LOGCONFIG(tag, "%s Speed count: %d", prefix, this->get_traits().supported_speed_count()); } - if (this->get_traits().supports_oscillation()) + if (this->get_traits().supports_oscillation()) { ESP_LOGCONFIG(tag, "%s Oscillation: YES", prefix); - if (this->get_traits().supports_direction()) + } + if (this->get_traits().supports_direction()) { ESP_LOGCONFIG(tag, "%s Direction: YES", prefix); + } } -uint32_t Fan::hash_base() { return 418001110UL; } } // namespace fan } // namespace esphome diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index cafb5843d1..f9d317e675 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -16,13 +16,6 @@ namespace fan { (obj)->dump_traits_(TAG, prefix); \ } -/// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon -enum ESPDEPRECATED("FanSpeed is deprecated.", "2021.9") FanSpeed { - FAN_SPEED_LOW = 0, ///< The fan is running on low speed. - FAN_SPEED_MEDIUM = 1, ///< The fan is running on medium speed. - FAN_SPEED_HIGH = 2 ///< The fan is running on high/full speed. -}; - /// Simple enum to represent the direction of a fan. enum class FanDirection { FORWARD = 0, REVERSE = 1 }; @@ -106,10 +99,6 @@ struct FanRestoreState { class Fan : public EntityBase { public: - Fan(); - /// Construct the fan with name. - explicit Fan(const std::string &name); - /// The current on/off state of the fan. bool state{false}; /// The current oscillation state of the fan. @@ -143,7 +132,6 @@ class Fan : public EntityBase { void save_state_(); void dump_traits_(const char *tag, const char *prefix); - uint32_t hash_base() override; CallbackManager state_callback_{}; ESPPreferenceObject rtc_; diff --git a/esphome/components/fan/fan_helpers.cpp b/esphome/components/fan/fan_helpers.cpp deleted file mode 100644 index 34883617e6..0000000000 --- a/esphome/components/fan/fan_helpers.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include -#include "fan_helpers.h" - -namespace esphome { -namespace fan { - -// This whole file is deprecated, don't warn about usage of deprecated types in here. -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - -FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels) { - const auto speed_ratio = static_cast(speed_level) / (supported_speed_levels + 1); - const auto legacy_level = clamp(static_cast(ceilf(speed_ratio * 3)), 1, 3); - return static_cast(legacy_level - 1); -} - -int speed_enum_to_level(FanSpeed speed, int supported_speed_levels) { - const auto enum_level = static_cast(speed) + 1; - const auto speed_level = roundf(enum_level / 3.0f * supported_speed_levels); - return static_cast(speed_level); -} - -} // namespace fan -} // namespace esphome diff --git a/esphome/components/fan/fan_helpers.h b/esphome/components/fan/fan_helpers.h deleted file mode 100644 index 8e8e3859bd..0000000000 --- a/esphome/components/fan/fan_helpers.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "fan.h" - -namespace esphome { -namespace fan { - -// Shut-up about usage of deprecated FanSpeed for a bit. -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - -ESPDEPRECATED("FanSpeed and speed_level_to_enum() are deprecated.", "2021.9") -FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels); -ESPDEPRECATED("FanSpeed and speed_enum_to_level() are deprecated.", "2021.9") -int speed_enum_to_level(FanSpeed speed, int supported_speed_levels); - -#pragma GCC diagnostic pop - -} // namespace fan -} // namespace esphome diff --git a/esphome/components/fan/fan_state.h b/esphome/components/fan/fan_state.h index 044ee59736..5926e700b0 100644 --- a/esphome/components/fan/fan_state.h +++ b/esphome/components/fan/fan_state.h @@ -15,7 +15,6 @@ enum ESPDEPRECATED("LegacyFanDirection members are deprecated, use FanDirection class ESPDEPRECATED("FanState is deprecated, use Fan instead.", "2022.2") FanState : public Fan, public Component { public: FanState() = default; - explicit FanState(const std::string &name) : Fan(name) {} /// Get the traits of this fan. FanTraits get_traits() override { return this->traits_; } diff --git a/esphome/components/fastled_clockless/light.py b/esphome/components/fastled_clockless/light.py index acf9488ae3..dc456d4959 100644 --- a/esphome/components/fastled_clockless/light.py +++ b/esphome/components/fastled_clockless/light.py @@ -49,7 +49,12 @@ CONFIG_SCHEMA = cv.All( } ), _validate, - cv.only_with_arduino, + cv.require_framework_version( + esp8266_arduino=cv.Version(2, 7, 4), + esp32_arduino=cv.Version(99, 0, 0), + max_version=True, + extra_message="Please see note on documentation for FastLED", + ), ) diff --git a/esphome/components/fastled_spi/light.py b/esphome/components/fastled_spi/light.py index a729fc015a..b3ce1722ee 100644 --- a/esphome/components/fastled_spi/light.py +++ b/esphome/components/fastled_spi/light.py @@ -33,7 +33,12 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DATA_RATE): cv.frequency, } ), - cv.only_with_arduino, + cv.require_framework_version( + esp8266_arduino=cv.Version(2, 7, 4), + esp32_arduino=cv.Version(99, 0, 0), + max_version=True, + extra_message="Please see note on documentation for FastLED", + ), ) diff --git a/esphome/components/feedback/__init__.py b/esphome/components/feedback/__init__.py new file mode 100644 index 0000000000..9ae2df986d --- /dev/null +++ b/esphome/components/feedback/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@ianchi"] diff --git a/esphome/components/feedback/cover.py b/esphome/components/feedback/cover.py new file mode 100644 index 0000000000..450eb967b1 --- /dev/null +++ b/esphome/components/feedback/cover.py @@ -0,0 +1,157 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import binary_sensor, cover +from esphome.const import ( + CONF_ASSUMED_STATE, + CONF_CLOSE_ACTION, + CONF_CLOSE_DURATION, + CONF_CLOSE_ENDSTOP, + CONF_ID, + CONF_OPEN_ACTION, + CONF_OPEN_DURATION, + CONF_OPEN_ENDSTOP, + CONF_STOP_ACTION, + CONF_MAX_DURATION, + CONF_UPDATE_INTERVAL, +) + +CONF_OPEN_SENSOR = "open_sensor" +CONF_CLOSE_SENSOR = "close_sensor" +CONF_OPEN_OBSTACLE_SENSOR = "open_obstacle_sensor" +CONF_CLOSE_OBSTACLE_SENSOR = "close_obstacle_sensor" +CONF_HAS_BUILT_IN_ENDSTOP = "has_built_in_endstop" +CONF_INFER_ENDSTOP_FROM_MOVEMENT = "infer_endstop_from_movement" +CONF_DIRECTION_CHANGE_WAIT_TIME = "direction_change_wait_time" +CONF_ACCELERATION_WAIT_TIME = "acceleration_wait_time" +CONF_OBSTACLE_ROLLBACK = "obstacle_rollback" + +endstop_ns = cg.esphome_ns.namespace("feedback") +FeedbackCover = endstop_ns.class_("FeedbackCover", cover.Cover, cg.Component) + + +def validate_infer_endstop(config): + if config[CONF_INFER_ENDSTOP_FROM_MOVEMENT] is True: + if config[CONF_HAS_BUILT_IN_ENDSTOP] is False: + raise cv.Invalid( + f"{CONF_INFER_ENDSTOP_FROM_MOVEMENT} can only be set if {CONF_HAS_BUILT_IN_ENDSTOP} is also set" + ) + + if CONF_OPEN_SENSOR not in config: + raise cv.Invalid( + f"{CONF_INFER_ENDSTOP_FROM_MOVEMENT} cannot be set if movement sensors are not supplied" + ) + + if CONF_OPEN_ENDSTOP in config or CONF_CLOSE_ENDSTOP in config: + raise cv.Invalid( + f"{CONF_INFER_ENDSTOP_FROM_MOVEMENT} cannot be set if endstop sensors are supplied" + ) + + return config + + +CONFIG_FEEDBACK_COVER_BASE_SCHEMA = cover.COVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(FeedbackCover), + cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_OPEN_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_OPEN_SENSOR): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_OPEN_OBSTACLE_SENSOR): cv.use_id(binary_sensor.BinarySensor), + cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), + cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_CLOSE_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_CLOSE_SENSOR): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_CLOSE_OBSTACLE_SENSOR): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds, + cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean, + cv.Optional(CONF_ASSUMED_STATE): cv.boolean, + cv.Optional( + CONF_UPDATE_INTERVAL, "1000ms" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_INFER_ENDSTOP_FROM_MOVEMENT, False): cv.boolean, + cv.Optional( + CONF_DIRECTION_CHANGE_WAIT_TIME + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_ACCELERATION_WAIT_TIME, "0s" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage, + }, +).extend(cv.COMPONENT_SCHEMA) + + +CONFIG_SCHEMA = cv.All( + CONFIG_FEEDBACK_COVER_BASE_SCHEMA, + cv.has_none_or_all_keys(CONF_OPEN_SENSOR, CONF_CLOSE_SENSOR), + validate_infer_endstop, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await cover.register_cover(var, config) + + # STOP + await automation.build_automation( + var.get_stop_trigger(), [], config[CONF_STOP_ACTION] + ) + + # OPEN + await automation.build_automation( + var.get_open_trigger(), [], config[CONF_OPEN_ACTION] + ) + cg.add(var.set_open_duration(config[CONF_OPEN_DURATION])) + if CONF_OPEN_ENDSTOP in config: + bin = await cg.get_variable(config[CONF_OPEN_ENDSTOP]) + cg.add(var.set_open_endstop(bin)) + if CONF_OPEN_SENSOR in config: + bin = await cg.get_variable(config[CONF_OPEN_SENSOR]) + cg.add(var.set_open_sensor(bin)) + if CONF_OPEN_OBSTACLE_SENSOR in config: + bin = await cg.get_variable(config[CONF_OPEN_OBSTACLE_SENSOR]) + cg.add(var.set_open_obstacle_sensor(bin)) + + # CLOSE + await automation.build_automation( + var.get_close_trigger(), [], config[CONF_CLOSE_ACTION] + ) + cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION])) + if CONF_CLOSE_ENDSTOP in config: + bin = await cg.get_variable(config[CONF_CLOSE_ENDSTOP]) + cg.add(var.set_close_endstop(bin)) + if CONF_CLOSE_SENSOR in config: + bin = await cg.get_variable(config[CONF_CLOSE_SENSOR]) + cg.add(var.set_close_sensor(bin)) + if CONF_CLOSE_OBSTACLE_SENSOR in config: + bin = await cg.get_variable(config[CONF_CLOSE_OBSTACLE_SENSOR]) + cg.add(var.set_close_obstacle_sensor(bin)) + + # OTHER + if CONF_MAX_DURATION in config: + cg.add(var.set_max_duration(config[CONF_MAX_DURATION])) + + cg.add(var.set_has_built_in_endstop(config[CONF_HAS_BUILT_IN_ENDSTOP])) + + if CONF_ASSUMED_STATE in config: + cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) + else: + cg.add( + var.set_assumed_state( + not ( + (CONF_CLOSE_ENDSTOP in config and CONF_OPEN_ENDSTOP in config) + or config[CONF_INFER_ENDSTOP_FROM_MOVEMENT] + ) + ) + ) + + cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) + cg.add(var.set_infer_endstop(config[CONF_INFER_ENDSTOP_FROM_MOVEMENT])) + if CONF_DIRECTION_CHANGE_WAIT_TIME in config: + cg.add( + var.set_direction_change_waittime(config[CONF_DIRECTION_CHANGE_WAIT_TIME]) + ) + cg.add(var.set_acceleration_wait_time(config[CONF_ACCELERATION_WAIT_TIME])) + cg.add(var.set_obstacle_rollback(config[CONF_OBSTACLE_ROLLBACK])) diff --git a/esphome/components/feedback/feedback_cover.cpp b/esphome/components/feedback/feedback_cover.cpp new file mode 100644 index 0000000000..117c626f58 --- /dev/null +++ b/esphome/components/feedback/feedback_cover.cpp @@ -0,0 +1,446 @@ +#include "feedback_cover.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace feedback { + +static const char *const TAG = "feedback.cover"; + +using namespace esphome::cover; + +void FeedbackCover::setup() { + auto restore = this->restore_state_(); + + if (restore.has_value()) { + restore->apply(this); + } else { + // if no other information, assume half open + this->position = 0.5f; + } + this->current_operation = COVER_OPERATION_IDLE; + +#ifdef USE_BINARY_SENSOR + // if available, get position from endstop sensors + if (this->open_endstop_ != nullptr && this->open_endstop_->state) { + this->position = COVER_OPEN; + } else if (this->close_endstop_ != nullptr && this->close_endstop_->state) { + this->position = COVER_CLOSED; + } + + // if available, get moving state from sensors + if (this->open_feedback_ != nullptr && this->open_feedback_->state) { + this->current_operation = COVER_OPERATION_OPENING; + } else if (this->close_feedback_ != nullptr && this->close_feedback_->state) { + this->current_operation = COVER_OPERATION_CLOSING; + } +#endif + + this->last_recompute_time_ = this->start_dir_time_ = millis(); +} + +CoverTraits FeedbackCover::get_traits() { + auto traits = CoverTraits(); + traits.set_supports_stop(true); + traits.set_supports_position(true); + traits.set_supports_toggle(true); + traits.set_is_assumed_state(this->assumed_state_); + return traits; +} + +void FeedbackCover::dump_config() { + LOG_COVER("", "Endstop Cover", this); + ESP_LOGCONFIG(TAG, " Open Duration: %.1fs", this->open_duration_ / 1e3f); +#ifdef USE_BINARY_SENSOR + LOG_BINARY_SENSOR(" ", "Open Endstop", this->open_endstop_); + LOG_BINARY_SENSOR(" ", "Open Feedback", this->open_feedback_); + LOG_BINARY_SENSOR(" ", "Open Obstacle", this->open_obstacle_); +#endif + ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f); +#ifdef USE_BINARY_SENSOR + LOG_BINARY_SENSOR(" ", "Close Endstop", this->close_endstop_); + LOG_BINARY_SENSOR(" ", "Close Feedback", this->close_feedback_); + LOG_BINARY_SENSOR(" ", "Close Obstacle", this->close_obstacle_); +#endif + if (this->has_built_in_endstop_) { + ESP_LOGCONFIG(TAG, " Has builtin endstop: YES"); + } + if (this->infer_endstop_) { + ESP_LOGCONFIG(TAG, " Infer endstop from movement: YES"); + } + if (this->max_duration_ < UINT32_MAX) { + ESP_LOGCONFIG(TAG, " Max Duration: %.1fs", this->max_duration_ / 1e3f); + } + if (this->direction_change_waittime_.has_value()) { + ESP_LOGCONFIG(TAG, " Direction change wait time: %.1fs", *this->direction_change_waittime_ / 1e3f); + } + if (this->acceleration_wait_time_) { + ESP_LOGCONFIG(TAG, " Acceleration wait time: %.1fs", this->acceleration_wait_time_ / 1e3f); + } +#ifdef USE_BINARY_SENSOR + if (this->obstacle_rollback_ && (this->open_obstacle_ != nullptr || this->close_obstacle_ != nullptr)) { + ESP_LOGCONFIG(TAG, " Obstacle rollback: %.1f%%", this->obstacle_rollback_ * 100); + } +#endif +} + +#ifdef USE_BINARY_SENSOR + +void FeedbackCover::set_open_sensor(binary_sensor::BinarySensor *open_feedback) { + this->open_feedback_ = open_feedback; + + // setup callbacks to react to sensor changes + open_feedback->add_on_state_callback([this](bool state) { + ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED"); + this->recompute_position_(); + if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_OPENING) { + this->endstop_reached_(true); + } + this->set_current_operation_(state ? COVER_OPERATION_OPENING : COVER_OPERATION_IDLE, false); + }); +} + +void FeedbackCover::set_close_sensor(binary_sensor::BinarySensor *close_feedback) { + this->close_feedback_ = close_feedback; + + close_feedback->add_on_state_callback([this](bool state) { + ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED"); + this->recompute_position_(); + if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_CLOSING) { + this->endstop_reached_(false); + } + + this->set_current_operation_(state ? COVER_OPERATION_CLOSING : COVER_OPERATION_IDLE, false); + }); +} + +void FeedbackCover::set_open_endstop(binary_sensor::BinarySensor *open_endstop) { + this->open_endstop_ = open_endstop; + open_endstop->add_on_state_callback([this](bool state) { + if (state) { + this->endstop_reached_(true); + } + }); +} + +void FeedbackCover::set_close_endstop(binary_sensor::BinarySensor *close_endstop) { + this->close_endstop_ = close_endstop; + close_endstop->add_on_state_callback([this](bool state) { + if (state) { + this->endstop_reached_(false); + } + }); +} +#endif + +void FeedbackCover::endstop_reached_(bool open_endstop) { + const uint32_t now = millis(); + + this->position = open_endstop ? COVER_OPEN : COVER_CLOSED; + + // only act if endstop activated while moving in the right direction, in case we are coming back + // from a position slightly past the endpoint + if (this->current_trigger_operation_ == (open_endstop ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING)) { + float dur = (now - this->start_dir_time_) / 1e3f; + ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(), open_endstop ? "Open" : "Close", dur); + + // if there is no external mechanism, stop the cover + if (!this->has_built_in_endstop_) { + this->start_direction_(COVER_OPERATION_IDLE); + } else { + this->set_current_operation_(COVER_OPERATION_IDLE, true); + } + } + + // always sync position and publish + this->publish_state(); + this->last_publish_time_ = now; +} + +void FeedbackCover::set_current_operation_(cover::CoverOperation operation, bool is_triggered) { + if (is_triggered) { + this->current_trigger_operation_ = operation; + } + + // if it is setting the actual operation (not triggered one) or + // if we don't have moving sensor, we operate in optimistic mode, assuming actions take place immediately + // thus, triggered operation always sets current operation. + // otherwise, current operation comes from sensor, and may differ from requested operation + // this might be from delays or complex actions, or because the movement was not trigger by the component + // but initiated externally + +#ifdef USE_BINARY_SENSOR + if (!is_triggered || (this->open_feedback_ == nullptr || this->close_feedback_ == nullptr)) +#endif + { + auto now = millis(); + this->current_operation = operation; + this->start_dir_time_ = this->last_recompute_time_ = now; + this->publish_state(); + this->last_publish_time_ = now; + } +} + +#ifdef USE_BINARY_SENSOR +void FeedbackCover::set_close_obstacle_sensor(binary_sensor::BinarySensor *close_obstacle) { + this->close_obstacle_ = close_obstacle; + + close_obstacle->add_on_state_callback([this](bool state) { + if (state && (this->current_operation == COVER_OPERATION_CLOSING || + this->current_trigger_operation_ == COVER_OPERATION_CLOSING)) { + ESP_LOGD(TAG, "'%s' - Close obstacle detected.", this->name_.c_str()); + this->start_direction_(COVER_OPERATION_IDLE); + + if (this->obstacle_rollback_) { + this->target_position_ = clamp(this->position + this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN); + this->start_direction_(COVER_OPERATION_OPENING); + } + } + }); +} + +void FeedbackCover::set_open_obstacle_sensor(binary_sensor::BinarySensor *open_obstacle) { + this->open_obstacle_ = open_obstacle; + + open_obstacle->add_on_state_callback([this](bool state) { + if (state && (this->current_operation == COVER_OPERATION_OPENING || + this->current_trigger_operation_ == COVER_OPERATION_OPENING)) { + ESP_LOGD(TAG, "'%s' - Open obstacle detected.", this->name_.c_str()); + this->start_direction_(COVER_OPERATION_IDLE); + + if (this->obstacle_rollback_) { + this->target_position_ = clamp(this->position - this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN); + this->start_direction_(COVER_OPERATION_CLOSING); + } + } + }); +} +#endif + +void FeedbackCover::loop() { + if (this->current_operation == COVER_OPERATION_IDLE) + return; + const uint32_t now = millis(); + + // Recompute position every loop cycle + this->recompute_position_(); + + // if we initiated the move, check if we reached position or max time + // (stoping from endstop sensor is handled in callback) + if (this->current_trigger_operation_ != COVER_OPERATION_IDLE) { + if (this->is_at_target_()) { + if (this->has_built_in_endstop_ && + (this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED)) { + // Don't trigger stop, let the cover stop by itself. + this->set_current_operation_(COVER_OPERATION_IDLE, true); + } else { + this->start_direction_(COVER_OPERATION_IDLE); + } + } else if (now - this->start_dir_time_ > this->max_duration_) { + ESP_LOGD(TAG, "'%s' - Max duration reached. Stopping cover.", this->name_.c_str()); + this->start_direction_(COVER_OPERATION_IDLE); + } + } + + // update current position at requested interval, regardless of who started the movement + // so that we also update UI if there was an external movement + // don´t save intermediate positions + if (now - this->last_publish_time_ > this->update_interval_) { + this->publish_state(false); + this->last_publish_time_ = now; + } +} + +void FeedbackCover::control(const CoverCall &call) { + // stop action logic + if (call.get_stop()) { + this->start_direction_(COVER_OPERATION_IDLE); + } else if (call.get_toggle().has_value()) { + // toggle action logic: OPEN - STOP - CLOSE + if (this->current_trigger_operation_ != COVER_OPERATION_IDLE) { + this->start_direction_(COVER_OPERATION_IDLE); + } else { + if (this->position == COVER_CLOSED || this->last_operation_ == COVER_OPERATION_CLOSING) { + this->target_position_ = COVER_OPEN; + this->start_direction_(COVER_OPERATION_OPENING); + } else { + this->target_position_ = COVER_CLOSED; + this->start_direction_(COVER_OPERATION_CLOSING); + } + } + } else if (call.get_position().has_value()) { + // go to position action + auto pos = *call.get_position(); + if (pos == this->position) { + // already at target, + + // for covers with built in end stop, if we don´t have sensors we should send the command again + // to make sure the assumed state is not wrong + if (this->has_built_in_endstop_ && ((pos == COVER_OPEN +#ifdef USE_BINARY_SENSOR + && this->open_endstop_ == nullptr +#endif + && !this->infer_endstop_) || + (pos == COVER_CLOSED +#ifdef USE_BINARY_SENSOR + && this->close_endstop_ == nullptr +#endif + && !this->infer_endstop_))) { + this->target_position_ = pos; + this->start_direction_(pos == COVER_CLOSED ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING); + } else if (this->current_operation != COVER_OPERATION_IDLE || + this->current_trigger_operation_ != COVER_OPERATION_IDLE) { + // if we are moving, stop + this->start_direction_(COVER_OPERATION_IDLE); + } + } else { + this->target_position_ = pos; + this->start_direction_(pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING); + } + } +} + +void FeedbackCover::stop_prev_trigger_() { + if (this->direction_change_waittime_.has_value()) { + this->cancel_timeout("direction_change"); + } + if (this->prev_command_trigger_ != nullptr) { + this->prev_command_trigger_->stop_action(); + this->prev_command_trigger_ = nullptr; + } +} + +bool FeedbackCover::is_at_target_() const { + // if initiated externally, current operation might be different from + // operation that was triggered, thus evaluate position against what was asked + + switch (this->current_trigger_operation_) { + case COVER_OPERATION_OPENING: + return this->position >= this->target_position_; + case COVER_OPERATION_CLOSING: + return this->position <= this->target_position_; + case COVER_OPERATION_IDLE: + return this->current_operation == COVER_OPERATION_IDLE; + default: + return true; + } +} +void FeedbackCover::start_direction_(CoverOperation dir) { + Trigger<> *trig; + +#ifdef USE_BINARY_SENSOR + binary_sensor::BinarySensor *obstacle{nullptr}; +#endif + + switch (dir) { + case COVER_OPERATION_IDLE: + trig = this->stop_trigger_; + break; + case COVER_OPERATION_OPENING: + this->last_operation_ = dir; + trig = this->open_trigger_; +#ifdef USE_BINARY_SENSOR + obstacle = this->open_obstacle_; +#endif + break; + case COVER_OPERATION_CLOSING: + this->last_operation_ = dir; + trig = this->close_trigger_; +#ifdef USE_BINARY_SENSOR + obstacle = this->close_obstacle_; +#endif + break; + default: + return; + } + + this->stop_prev_trigger_(); + +#ifdef USE_BINARY_SENSOR + // check if there is an obstacle to start the new operation -> abort without any change + // the case when an obstacle appears while moving is handled in the callback + if (obstacle != nullptr && obstacle->state) { + ESP_LOGD(TAG, "'%s' - %s obstacle detected. Action not started.", this->name_.c_str(), + dir == COVER_OPERATION_OPENING ? "Open" : "Close"); + return; + } +#endif + + // if we are moving and need to move in the opposite direction + // check if we have a wait time + if (this->direction_change_waittime_.has_value() && dir != COVER_OPERATION_IDLE && + this->current_operation != COVER_OPERATION_IDLE && dir != this->current_operation) { + ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str()); + this->start_direction_(COVER_OPERATION_IDLE); + + this->set_timeout("direction_change", *this->direction_change_waittime_, + [this, dir]() { this->start_direction_(dir); }); + + } else { + this->set_current_operation_(dir, true); + this->prev_command_trigger_ = trig; + ESP_LOGD(TAG, "'%s' - Firing '%s' trigger.", this->name_.c_str(), + dir == COVER_OPERATION_OPENING ? "OPEN" + : dir == COVER_OPERATION_CLOSING ? "CLOSE" + : "STOP"); + trig->trigger(); + } +} + +void FeedbackCover::recompute_position_() { + if (this->current_operation == COVER_OPERATION_IDLE) + return; + + const uint32_t now = millis(); + float dir; + float action_dur; + float min_pos; + float max_pos; + + // endstop sensors update position from their callbacks, and sets the fully open/close value + // If we have endstop, estimation never reaches the fully open/closed state. + // but if movement continues past corresponding endstop (inertia), keep the fully open/close state + + switch (this->current_operation) { + case COVER_OPERATION_OPENING: + dir = 1.0f; + action_dur = this->open_duration_; + min_pos = COVER_CLOSED; + max_pos = ( +#ifdef USE_BINARY_SENSOR + this->open_endstop_ != nullptr || +#endif + this->infer_endstop_) && + this->position < COVER_OPEN + ? 0.99f + : COVER_OPEN; + break; + case COVER_OPERATION_CLOSING: + dir = -1.0f; + action_dur = this->close_duration_; + min_pos = ( +#ifdef USE_BINARY_SENSOR + this->close_endstop_ != nullptr || +#endif + this->infer_endstop_) && + this->position > COVER_CLOSED + ? 0.01f + : COVER_CLOSED; + max_pos = COVER_OPEN; + break; + default: + return; + } + + // check if we have an acceleration_wait_time, and remove from position computation + if (now > (this->start_dir_time_ + this->acceleration_wait_time_)) { + this->position += + dir * (now - std::max(this->start_dir_time_ + this->acceleration_wait_time_, this->last_recompute_time_)) / + (action_dur - this->acceleration_wait_time_); + this->position = clamp(this->position, min_pos, max_pos); + } + this->last_recompute_time_ = now; +} + +} // namespace feedback +} // namespace esphome diff --git a/esphome/components/feedback/feedback_cover.h b/esphome/components/feedback/feedback_cover.h new file mode 100644 index 0000000000..7e107aebcd --- /dev/null +++ b/esphome/components/feedback/feedback_cover.h @@ -0,0 +1,90 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#include "esphome/components/cover/cover.h" + +namespace esphome { +namespace feedback { + +class FeedbackCover : public cover::Cover, public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; }; + + Trigger<> *get_open_trigger() const { return this->open_trigger_; } + Trigger<> *get_close_trigger() const { return this->close_trigger_; } + Trigger<> *get_stop_trigger() const { return this->stop_trigger_; } + +#ifdef USE_BINARY_SENSOR + void set_open_endstop(binary_sensor::BinarySensor *open_endstop); + void set_open_sensor(binary_sensor::BinarySensor *open_feedback); + void set_open_obstacle_sensor(binary_sensor::BinarySensor *open_obstacle); + void set_close_endstop(binary_sensor::BinarySensor *close_endstop); + void set_close_sensor(binary_sensor::BinarySensor *close_feedback); + void set_close_obstacle_sensor(binary_sensor::BinarySensor *close_obstacle); +#endif + void set_open_duration(uint32_t duration) { this->open_duration_ = duration; } + void set_close_duration(uint32_t duration) { this->close_duration_ = duration; } + void set_has_built_in_endstop(bool value) { this->has_built_in_endstop_ = value; } + void set_assumed_state(bool value) { this->assumed_state_ = value; } + void set_max_duration(uint32_t max_duration) { this->max_duration_ = max_duration; } + void set_obstacle_rollback(float obstacle_rollback) { this->obstacle_rollback_ = obstacle_rollback; } + void set_update_interval(uint32_t interval) { this->update_interval_ = interval; } + void set_infer_endstop(bool infer_endstop) { this->infer_endstop_ = infer_endstop; } + void set_direction_change_waittime(uint32_t waittime) { this->direction_change_waittime_ = waittime; } + void set_acceleration_wait_time(uint32_t waittime) { this->acceleration_wait_time_ = waittime; } + + cover::CoverTraits get_traits() override; + + protected: + void control(const cover::CoverCall &call) override; + void stop_prev_trigger_(); + bool is_at_target_() const; + void start_direction_(cover::CoverOperation dir); + void update_operation_(cover::CoverOperation dir); + void endstop_reached_(bool open_endstop); + void recompute_position_(); + void set_current_operation_(cover::CoverOperation operation, bool is_triggered); + +#ifdef USE_BINARY_SENSOR + binary_sensor::BinarySensor *open_endstop_{nullptr}; + binary_sensor::BinarySensor *close_endstop_{nullptr}; + binary_sensor::BinarySensor *open_feedback_{nullptr}; + binary_sensor::BinarySensor *close_feedback_{nullptr}; + binary_sensor::BinarySensor *open_obstacle_{nullptr}; + binary_sensor::BinarySensor *close_obstacle_{nullptr}; + +#endif + Trigger<> *open_trigger_{new Trigger<>()}; + Trigger<> *close_trigger_{new Trigger<>()}; + Trigger<> *stop_trigger_{new Trigger<>()}; + + uint32_t open_duration_{0}; + uint32_t close_duration_{0}; + uint32_t max_duration_{UINT32_MAX}; + optional direction_change_waittime_{}; + uint32_t acceleration_wait_time_{0}; + bool has_built_in_endstop_{false}; + bool assumed_state_{false}; + bool infer_endstop_{false}; + float obstacle_rollback_{0}; + + cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING}; + cover::CoverOperation current_trigger_operation_{cover::COVER_OPERATION_IDLE}; + Trigger<> *prev_command_trigger_{nullptr}; + uint32_t last_recompute_time_{0}; + uint32_t start_dir_time_{0}; + uint32_t last_publish_time_{0}; + float target_position_{0}; + uint32_t update_interval_{1000}; +}; + +} // namespace feedback +} // namespace esphome diff --git a/esphome/components/fingerprint_grow/binary_sensor.py b/esphome/components/fingerprint_grow/binary_sensor.py index f432ef92cc..8572919e36 100644 --- a/esphome/components/fingerprint_grow/binary_sensor.py +++ b/esphome/components/fingerprint_grow/binary_sensor.py @@ -6,7 +6,7 @@ from . import CONF_FINGERPRINT_GROW_ID, FingerprintGrowComponent DEPENDENCIES = ["fingerprint_grow"] -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend( { cv.GenerateID(CONF_FINGERPRINT_GROW_ID): cv.use_id(FingerprintGrowComponent), cv.Optional(CONF_ICON, default=ICON_KEY_PLUS): cv.icon, diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index 3b8c52fea2..4043f32dcb 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -51,7 +51,7 @@ void FingerprintGrowComponent::update() { void FingerprintGrowComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up Grow Fingerprint Reader..."); if (this->check_password_()) { - if (this->new_password_ != nullptr) { + if (this->new_password_ != -1) { if (this->set_password_()) return; } else { @@ -77,10 +77,12 @@ void FingerprintGrowComponent::finish_enrollment(uint8_t result) { this->enrollment_done_callback_.call(this->enrollment_slot_); this->get_fingerprint_count_(); } else { - this->enrollment_failed_callback_.call(this->enrollment_slot_); + if (this->enrollment_slot_ != ENROLLMENT_SLOT_UNUSED) { + this->enrollment_failed_callback_.call(this->enrollment_slot_); + } } this->enrollment_image_ = 0; - this->enrollment_slot_ = 0; + this->enrollment_slot_ = ENROLLMENT_SLOT_UNUSED; if (this->enrolling_binary_sensor_ != nullptr) { this->enrolling_binary_sensor_->publish_state(false); } @@ -95,7 +97,7 @@ void FingerprintGrowComponent::scan_and_match_() { } if (this->scan_image_(1) == OK) { this->waiting_removal_ = true; - this->data_ = {SEARCH, 0x01, 0x00, 0x00, (uint8_t)(this->capacity_ >> 8), (uint8_t)(this->capacity_ & 0xFF)}; + this->data_ = {SEARCH, 0x01, 0x00, 0x00, (uint8_t) (this->capacity_ >> 8), (uint8_t) (this->capacity_ & 0xFF)}; switch (this->send_command_()) { case OK: { ESP_LOGD(TAG, "Fingerprint matched"); @@ -171,7 +173,7 @@ uint8_t FingerprintGrowComponent::save_fingerprint_() { } ESP_LOGI(TAG, "Storing model"); - this->data_ = {STORE, 0x01, (uint8_t)(this->enrollment_slot_ >> 8), (uint8_t)(this->enrollment_slot_ & 0xFF)}; + this->data_ = {STORE, 0x01, (uint8_t) (this->enrollment_slot_ >> 8), (uint8_t) (this->enrollment_slot_ & 0xFF)}; switch (this->send_command_()) { case OK: ESP_LOGI(TAG, "Stored model"); @@ -188,8 +190,8 @@ uint8_t FingerprintGrowComponent::save_fingerprint_() { bool FingerprintGrowComponent::check_password_() { ESP_LOGD(TAG, "Checking password"); - this->data_ = {VERIFY_PASSWORD, (uint8_t)(this->password_ >> 24), (uint8_t)(this->password_ >> 16), - (uint8_t)(this->password_ >> 8), (uint8_t)(this->password_ & 0xFF)}; + this->data_ = {VERIFY_PASSWORD, (uint8_t) (this->password_ >> 24), (uint8_t) (this->password_ >> 16), + (uint8_t) (this->password_ >> 8), (uint8_t) (this->password_ & 0xFF)}; switch (this->send_command_()) { case OK: ESP_LOGD(TAG, "Password verified"); @@ -202,9 +204,9 @@ bool FingerprintGrowComponent::check_password_() { } bool FingerprintGrowComponent::set_password_() { - ESP_LOGI(TAG, "Setting new password: %d", *this->new_password_); - this->data_ = {SET_PASSWORD, (uint8_t)(*this->new_password_ >> 24), (uint8_t)(*this->new_password_ >> 16), - (uint8_t)(*this->new_password_ >> 8), (uint8_t)(*this->new_password_ & 0xFF)}; + ESP_LOGI(TAG, "Setting new password: %d", this->new_password_); + this->data_ = {SET_PASSWORD, (uint8_t) (this->new_password_ >> 24), (uint8_t) (this->new_password_ >> 16), + (uint8_t) (this->new_password_ >> 8), (uint8_t) (this->new_password_ & 0xFF)}; if (this->send_command_() == OK) { ESP_LOGI(TAG, "New password successfully set"); ESP_LOGI(TAG, "Define the new password in your configuration and reflash now"); @@ -250,7 +252,7 @@ void FingerprintGrowComponent::get_fingerprint_count_() { void FingerprintGrowComponent::delete_fingerprint(uint16_t finger_id) { ESP_LOGI(TAG, "Deleting fingerprint in slot %d", finger_id); - this->data_ = {DELETE, (uint8_t)(finger_id >> 8), (uint8_t)(finger_id & 0xFF), 0x00, 0x01}; + this->data_ = {DELETE, (uint8_t) (finger_id >> 8), (uint8_t) (finger_id & 0xFF), 0x00, 0x01}; switch (this->send_command_()) { case OK: ESP_LOGI(TAG, "Deleted fingerprint"); @@ -320,8 +322,8 @@ void FingerprintGrowComponent::aura_led_control(uint8_t state, uint8_t speed, ui } uint8_t FingerprintGrowComponent::send_command_() { - this->write((uint8_t)(START_CODE >> 8)); - this->write((uint8_t)(START_CODE & 0xFF)); + this->write((uint8_t) (START_CODE >> 8)); + this->write((uint8_t) (START_CODE & 0xFF)); this->write(this->address_[0]); this->write(this->address_[1]); this->write(this->address_[2]); @@ -329,8 +331,8 @@ uint8_t FingerprintGrowComponent::send_command_() { this->write(COMMAND); uint16_t wire_length = this->data_.size() + 2; - this->write((uint8_t)(wire_length >> 8)); - this->write((uint8_t)(wire_length & 0xFF)); + this->write((uint8_t) (wire_length >> 8)); + this->write((uint8_t) (wire_length & 0xFF)); uint16_t sum = ((wire_length) >> 8) + ((wire_length) &0xFF) + COMMAND; for (auto data : this->data_) { @@ -338,8 +340,8 @@ uint8_t FingerprintGrowComponent::send_command_() { sum += data; } - this->write((uint8_t)(sum >> 8)); - this->write((uint8_t)(sum & 0xFF)); + this->write((uint8_t) (sum >> 8)); + this->write((uint8_t) (sum & 0xFF)); this->data_.clear(); @@ -354,11 +356,11 @@ uint8_t FingerprintGrowComponent::send_command_() { byte = this->read(); switch (idx) { case 0: - if (byte != (uint8_t)(START_CODE >> 8)) + if (byte != (uint8_t) (START_CODE >> 8)) continue; break; case 1: - if (byte != (uint8_t)(START_CODE & 0xFF)) { + if (byte != (uint8_t) (START_CODE & 0xFF)) { idx = 0; continue; } diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 7ec253ff3a..f414146e64 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -6,11 +6,15 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/uart/uart.h" +#include + namespace esphome { namespace fingerprint_grow { static const uint16_t START_CODE = 0xEF01; +static const uint16_t ENROLLMENT_SLOT_UNUSED = 0xFFFF; + enum GrowPacketType { COMMAND = 0x01, DATA = 0x02, @@ -89,14 +93,14 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic void dump_config() override; void set_address(uint32_t address) { - this->address_[0] = (uint8_t)(address >> 24); - this->address_[1] = (uint8_t)(address >> 16); - this->address_[2] = (uint8_t)(address >> 8); - this->address_[3] = (uint8_t)(address & 0xFF); + this->address_[0] = (uint8_t) (address >> 24); + this->address_[1] = (uint8_t) (address >> 16); + this->address_[2] = (uint8_t) (address >> 8); + this->address_[3] = (uint8_t) (address & 0xFF); } void set_sensing_pin(GPIOPin *sensing_pin) { this->sensing_pin_ = sensing_pin; } void set_password(uint32_t password) { this->password_ = password; } - void set_new_password(uint32_t new_password) { this->new_password_ = &new_password; } + void set_new_password(uint32_t new_password) { this->new_password_ = new_password; } void set_fingerprint_count_sensor(sensor::Sensor *fingerprint_count_sensor) { this->fingerprint_count_sensor_ = fingerprint_count_sensor; } @@ -153,10 +157,10 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic uint8_t address_[4] = {0xFF, 0xFF, 0xFF, 0xFF}; uint16_t capacity_ = 64; uint32_t password_ = 0x0; - uint32_t *new_password_{nullptr}; + uint32_t new_password_ = -1; GPIOPin *sensing_pin_{nullptr}; uint8_t enrollment_image_ = 0; - uint16_t enrollment_slot_ = 0; + uint16_t enrollment_slot_ = ENROLLMENT_SLOT_UNUSED; uint8_t enrollment_buffers_ = 5; bool waiting_removal_ = false; uint32_t last_aura_led_control_ = 0; diff --git a/esphome/components/fingerprint_grow/sensor.py b/esphome/components/fingerprint_grow/sensor.py index 4ae670743d..ed4e431dcc 100644 --- a/esphome/components/fingerprint_grow/sensor.py +++ b/esphome/components/fingerprint_grow/sensor.py @@ -14,7 +14,6 @@ from esphome.const import ( ICON_DATABASE, ICON_FINGERPRINT, ICON_SECURITY, - STATE_CLASS_NONE, ) from . import CONF_FINGERPRINT_GROW_ID, FingerprintGrowComponent @@ -26,36 +25,30 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_FINGERPRINT_COUNT): sensor.sensor_schema( icon=ICON_FINGERPRINT, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_STATUS): sensor.sensor_schema( accuracy_decimals=0, - state_class=STATE_CLASS_NONE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_CAPACITY): sensor.sensor_schema( icon=ICON_DATABASE, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_SECURITY_LEVEL): sensor.sensor_schema( icon=ICON_SECURITY, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_LAST_FINGER_ID): sensor.sensor_schema( icon=ICON_ACCOUNT, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_LAST_CONFIDENCE): sensor.sensor_schema( icon=ICON_ACCOUNT_CHECK, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 6af5be45d4..7a314bb032 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -1,18 +1,38 @@ import functools +from pathlib import Path +import hashlib +import os +import re + +import requests from esphome import core -from esphome.components import display import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_FILE, CONF_GLYPHS, CONF_ID, CONF_RAW_DATA_ID, CONF_SIZE +from esphome.helpers import copy_file_if_changed +from esphome.const import ( + CONF_FAMILY, + CONF_FILE, + CONF_GLYPHS, + CONF_ID, + CONF_RAW_DATA_ID, + CONF_TYPE, + CONF_SIZE, + CONF_PATH, + CONF_WEIGHT, +) from esphome.core import CORE, HexInt + +DOMAIN = "font" DEPENDENCIES = ["display"] MULTI_CONF = True -Font = display.display_ns.class_("Font") -Glyph = display.display_ns.class_("Glyph") -GlyphData = display.display_ns.struct("GlyphData") +font_ns = cg.esphome_ns.namespace("font") + +Font = font_ns.class_("Font") +Glyph = font_ns.class_("Glyph") +GlyphData = font_ns.struct("GlyphData") def validate_glyphs(value): @@ -71,6 +91,150 @@ def validate_truetype_file(value): return cv.file_(value) +def _compute_local_font_dir(name) -> Path: + base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN + h = hashlib.new("sha256") + h.update(name.encode()) + return base_dir / h.hexdigest()[:8] + + +def _compute_gfonts_local_path(value) -> Path: + name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1" + return _compute_local_font_dir(name) / "font.ttf" + + +TYPE_LOCAL = "local" +TYPE_LOCAL_BITMAP = "local_bitmap" +TYPE_GFONTS = "gfonts" +LOCAL_SCHEMA = cv.Schema( + { + cv.Required(CONF_PATH): validate_truetype_file, + } +) + +LOCAL_BITMAP_SCHEMA = cv.Schema( + { + cv.Required(CONF_PATH): cv.file_, + } +) + +CONF_ITALIC = "italic" +FONT_WEIGHTS = { + "thin": 100, + "extra-light": 200, + "light": 300, + "regular": 400, + "medium": 500, + "semi-bold": 600, + "bold": 700, + "extra-bold": 800, + "black": 900, +} + + +def validate_weight_name(value): + return FONT_WEIGHTS[cv.one_of(*FONT_WEIGHTS, lower=True, space="-")(value)] + + +def download_gfonts(value): + wght = value[CONF_WEIGHT] + if value[CONF_ITALIC]: + wght = f"1,{wght}" + name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}" + url = f"https://fonts.googleapis.com/css2?family={value[CONF_FAMILY]}:wght@{wght}" + + path = _compute_gfonts_local_path(value) + if path.is_file(): + return value + try: + req = requests.get(url, timeout=30) + req.raise_for_status() + except requests.exceptions.RequestException as e: + raise cv.Invalid( + f"Could not download font for {name}, please check the fonts exists " + f"at google fonts ({e})" + ) + match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text) + if match is None: + raise cv.Invalid( + f"Could not extract ttf file from gfonts response for {name}, " + f"please report this." + ) + + ttf_url = match.group(1) + try: + req = requests.get(ttf_url, timeout=30) + req.raise_for_status() + except requests.exceptions.RequestException as e: + raise cv.Invalid(f"Could not download ttf file for {name} ({ttf_url}): {e}") + + path.parent.mkdir(exist_ok=True, parents=True) + path.write_bytes(req.content) + return value + + +GFONTS_SCHEMA = cv.All( + { + cv.Required(CONF_FAMILY): cv.string_strict, + cv.Optional(CONF_WEIGHT, default="regular"): cv.Any( + cv.int_, validate_weight_name + ), + cv.Optional(CONF_ITALIC, default=False): cv.boolean, + }, + download_gfonts, +) + + +def validate_file_shorthand(value): + value = cv.string_strict(value) + if value.startswith("gfonts://"): + match = re.match(r"^gfonts://([^@]+)(@.+)?$", value) + if match is None: + raise cv.Invalid("Could not parse gfonts shorthand syntax, please check it") + family = match.group(1) + weight = match.group(2) + data = { + CONF_TYPE: TYPE_GFONTS, + CONF_FAMILY: family, + } + if weight is not None: + data[CONF_WEIGHT] = weight[1:] + return FILE_SCHEMA(data) + + if value.endswith(".pcf") or value.endswith(".bdf"): + return FILE_SCHEMA( + { + CONF_TYPE: TYPE_LOCAL_BITMAP, + CONF_PATH: value, + } + ) + + return FILE_SCHEMA( + { + CONF_TYPE: TYPE_LOCAL, + CONF_PATH: value, + } + ) + + +TYPED_FILE_SCHEMA = cv.typed_schema( + { + TYPE_LOCAL: LOCAL_SCHEMA, + TYPE_GFONTS: GFONTS_SCHEMA, + TYPE_LOCAL_BITMAP: LOCAL_BITMAP_SCHEMA, + } +) + + +def _file_schema(value): + if isinstance(value, str): + return validate_file_shorthand(value) + return TYPED_FILE_SCHEMA(value) + + +FILE_SCHEMA = cv.Schema(_file_schema) + + DEFAULT_GLYPHS = ( ' !"%()+=,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' ) @@ -79,7 +243,7 @@ CONF_RAW_GLYPH_ID = "raw_glyph_id" FONT_SCHEMA = cv.Schema( { cv.Required(CONF_ID): cv.declare_id(Font), - cv.Required(CONF_FILE): validate_truetype_file, + cv.Required(CONF_FILE): FILE_SCHEMA, cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs, cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1), cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), @@ -89,23 +253,121 @@ FONT_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA) +# PIL doesn't provide a consistent interface for both TrueType and bitmap +# fonts. So, we use our own wrappers to give us the consistency that we need. -async def to_code(config): + +class TrueTypeFontWrapper: + def __init__(self, font): + self.font = font + + def getoffset(self, glyph): + _, (offset_x, offset_y) = self.font.font.getsize(glyph) + return offset_x, offset_y + + def getmask(self, glyph, **kwargs): + return self.font.getmask(glyph, **kwargs) + + def getmetrics(self, glyphs): + return self.font.getmetrics() + + +class BitmapFontWrapper: + def __init__(self, font): + self.font = font + self.max_height = 0 + + def getoffset(self, glyph): + return 0, 0 + + def getmask(self, glyph, **kwargs): + return self.font.getmask(glyph, **kwargs) + + def getmetrics(self, glyphs): + max_height = 0 + for glyph in glyphs: + mask = self.getmask(glyph, mode="1") + _, height = mask.size + if height > max_height: + max_height = height + return (max_height, 0) + + +def convert_bitmap_to_pillow_font(filepath): + from PIL import PcfFontFile, BdfFontFile + + local_bitmap_font_file = _compute_local_font_dir(filepath) / os.path.basename( + filepath + ) + + copy_file_if_changed(filepath, local_bitmap_font_file) + + with open(local_bitmap_font_file, "rb") as fp: + try: + try: + p = PcfFontFile.PcfFontFile(fp) + except SyntaxError: + fp.seek(0) + p = BdfFontFile.BdfFontFile(fp) + + # Convert to pillow-formatted fonts, which have a .pil and .pbm extension. + p.save(local_bitmap_font_file) + except (SyntaxError, OSError) as err: + raise core.EsphomeError( + f"Failed to parse as bitmap font: '{filepath}': {err}" + ) + + local_pil_font_file = os.path.splitext(local_bitmap_font_file)[0] + ".pil" + return cv.file_(local_pil_font_file) + + +def load_bitmap_font(filepath): from PIL import ImageFont - path = CORE.relative_config_path(config[CONF_FILE]) + # Convert bpf and pcf files to pillow fonts, first. + pil_font_path = convert_bitmap_to_pillow_font(filepath) + try: - font = ImageFont.truetype(path, config[CONF_SIZE]) + font = ImageFont.load(str(pil_font_path)) + except Exception as e: + raise core.EsphomeError( + f"Failed to load bitmap font file: {pil_font_path} : {e}" + ) + + return BitmapFontWrapper(font) + + +def load_ttf_font(path, size): + from PIL import ImageFont + + try: + font = ImageFont.truetype(str(path), size) except Exception as e: raise core.EsphomeError(f"Could not load truetype file {path}: {e}") - ascent, descent = font.getmetrics() + return TrueTypeFontWrapper(font) + + +async def to_code(config): + conf = config[CONF_FILE] + if conf[CONF_TYPE] == TYPE_LOCAL_BITMAP: + font = load_bitmap_font(CORE.relative_config_path(conf[CONF_PATH])) + elif conf[CONF_TYPE] == TYPE_LOCAL: + path = CORE.relative_config_path(conf[CONF_PATH]) + font = load_ttf_font(path, config[CONF_SIZE]) + elif conf[CONF_TYPE] == TYPE_GFONTS: + path = _compute_gfonts_local_path(conf) + font = load_ttf_font(path, config[CONF_SIZE]) + else: + raise core.EsphomeError(f"Could not load font: unknown type: {conf[CONF_TYPE]}") + + ascent, descent = font.getmetrics(config[CONF_GLYPHS]) glyph_args = {} data = [] for glyph in config[CONF_GLYPHS]: mask = font.getmask(glyph, mode="1") - _, (offset_x, offset_y) = font.font.getsize(glyph) + offset_x, offset_y = font.getoffset(glyph) width, height = mask.size width8 = ((width + 7) // 8) * 8 glyph_data = [0] * (height * width8 // 8) diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp new file mode 100644 index 0000000000..fcb2bb1750 --- /dev/null +++ b/esphome/components/font/font.cpp @@ -0,0 +1,149 @@ +#include "font.h" + +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/color.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace font { + +static const char *const TAG = "font"; + +void Glyph::draw(int x_at, int y_start, display::DisplayBuffer *display, Color color) const { + int scan_x1, scan_y1, scan_width, scan_height; + this->scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height); + + const unsigned char *data = this->glyph_data_->data; + const int max_x = x_at + scan_x1 + scan_width; + const int max_y = y_start + scan_y1 + scan_height; + + for (int glyph_y = y_start + scan_y1; glyph_y < max_y; glyph_y++) { + for (int glyph_x = x_at + scan_x1; glyph_x < max_x; data++, glyph_x += 8) { + uint8_t pixel_data = progmem_read_byte(data); + const int pixel_max_x = std::min(max_x, glyph_x + 8); + + for (int pixel_x = glyph_x; pixel_x < pixel_max_x && pixel_data; pixel_x++, pixel_data <<= 1) { + if (pixel_data & 0x80) { + display->draw_pixel_at(pixel_x, glyph_y, color); + } + } + } + } +} +const char *Glyph::get_char() const { return this->glyph_data_->a_char; } +bool Glyph::compare_to(const char *str) const { + // 1 -> this->char_ + // 2 -> str + for (uint32_t i = 0;; i++) { + if (this->glyph_data_->a_char[i] == '\0') + return true; + if (str[i] == '\0') + return false; + if (this->glyph_data_->a_char[i] > str[i]) + return false; + if (this->glyph_data_->a_char[i] < str[i]) + return true; + } + // this should not happen + return false; +} +int Glyph::match_length(const char *str) const { + for (uint32_t i = 0;; i++) { + if (this->glyph_data_->a_char[i] == '\0') + return i; + if (str[i] != this->glyph_data_->a_char[i]) + return 0; + } + // this should not happen + return 0; +} +void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { + *x1 = this->glyph_data_->offset_x; + *y1 = this->glyph_data_->offset_y; + *width = this->glyph_data_->width; + *height = this->glyph_data_->height; +} + +Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) { + glyphs_.reserve(data_nr); + for (int i = 0; i < data_nr; ++i) + glyphs_.emplace_back(&data[i]); +} +int Font::match_next_glyph(const char *str, int *match_length) { + int lo = 0; + int hi = this->glyphs_.size() - 1; + while (lo != hi) { + int mid = (lo + hi + 1) / 2; + if (this->glyphs_[mid].compare_to(str)) { + lo = mid; + } else { + hi = mid - 1; + } + } + *match_length = this->glyphs_[lo].match_length(str); + if (*match_length <= 0) + return -1; + return lo; +} +void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) { + *baseline = this->baseline_; + *height = this->height_; + int i = 0; + int min_x = 0; + bool has_char = false; + int x = 0; + while (str[i] != '\0') { + int match_length; + int glyph_n = this->match_next_glyph(str + i, &match_length); + if (glyph_n < 0) { + // Unknown char, skip + if (!this->get_glyphs().empty()) + x += this->get_glyphs()[0].glyph_data_->width; + i++; + continue; + } + + const Glyph &glyph = this->glyphs_[glyph_n]; + if (!has_char) { + min_x = glyph.glyph_data_->offset_x; + } else { + min_x = std::min(min_x, x + glyph.glyph_data_->offset_x); + } + x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; + + i += match_length; + has_char = true; + } + *x_offset = min_x; + *width = x - min_x; +} +void Font::print(int x_start, int y_start, display::DisplayBuffer *display, Color color, const char *text) { + int i = 0; + int x_at = x_start; + while (text[i] != '\0') { + int match_length; + int glyph_n = this->match_next_glyph(text + i, &match_length); + if (glyph_n < 0) { + // Unknown char, skip + ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); + if (!this->get_glyphs().empty()) { + uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->width; + display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color); + x_at += glyph_width; + } + + i++; + continue; + } + + const Glyph &glyph = this->get_glyphs()[glyph_n]; + glyph.draw(x_at, y_start, display, color); + x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; + + i += match_length; + } +} + +} // namespace font +} // namespace esphome diff --git a/esphome/components/font/font.h b/esphome/components/font/font.h new file mode 100644 index 0000000000..d88ebd9be6 --- /dev/null +++ b/esphome/components/font/font.h @@ -0,0 +1,67 @@ +#pragma once + +#include "esphome/core/datatypes.h" +#include "esphome/core/color.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace font { + +class Font; + +struct GlyphData { + const char *a_char; + const uint8_t *data; + int offset_x; + int offset_y; + int width; + int height; +}; + +class Glyph { + public: + Glyph(const GlyphData *data) : glyph_data_(data) {} + + void draw(int x, int y, display::DisplayBuffer *display, Color color) const; + + const char *get_char() const; + + bool compare_to(const char *str) const; + + int match_length(const char *str) const; + + void scan_area(int *x1, int *y1, int *width, int *height) const; + + protected: + friend Font; + + const GlyphData *glyph_data_; +}; + +class Font : public display::BaseFont { + public: + /** Construct the font with the given glyphs. + * + * @param glyphs A vector of glyphs, must be sorted lexicographically. + * @param baseline The y-offset from the top of the text to the baseline. + * @param bottom The y-offset from the top of the text to the bottom (i.e. height). + */ + Font(const GlyphData *data, int data_nr, int baseline, int height); + + int match_next_glyph(const char *str, int *match_length); + + void print(int x_start, int y_start, display::DisplayBuffer *display, Color color, const char *text) override; + void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) override; + inline int get_baseline() { return this->baseline_; } + inline int get_height() { return this->height_; } + + const std::vector> &get_glyphs() const { return glyphs_; } + + protected: + std::vector> glyphs_; + int baseline_; + int height_; +}; + +} // namespace font +} // namespace esphome diff --git a/esphome/components/fs3000/__init__.py b/esphome/components/fs3000/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/fs3000/fs3000.cpp b/esphome/components/fs3000/fs3000.cpp new file mode 100644 index 0000000000..fb729ed0a0 --- /dev/null +++ b/esphome/components/fs3000/fs3000.cpp @@ -0,0 +1,107 @@ +#include "fs3000.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace fs3000 { + +static const char *const TAG = "fs3000"; + +void FS3000Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up FS3000..."); + + if (model_ == FIVE) { + // datasheet gives 9 points to interpolate from for the 1005 model + static const uint16_t RAW_DATA_POINTS_1005[9] = {409, 915, 1522, 2066, 2523, 2908, 3256, 3572, 3686}; + static const float MPS_DATA_POINTS_1005[9] = {0.0, 1.07, 2.01, 3.0, 3.97, 4.96, 5.98, 6.99, 7.23}; + + std::copy(RAW_DATA_POINTS_1005, RAW_DATA_POINTS_1005 + 9, this->raw_data_points_); + std::copy(MPS_DATA_POINTS_1005, MPS_DATA_POINTS_1005 + 9, this->mps_data_points_); + } else if (model_ == FIFTEEN) { + // datasheet gives 13 points to extrapolate from for the 1015 model + static const uint16_t RAW_DATA_POINTS_1015[13] = {409, 1203, 1597, 1908, 2187, 2400, 2629, + 2801, 3006, 3178, 3309, 3563, 3686}; + static const float MPS_DATA_POINTS_1015[13] = {0.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 13.0, 15.0}; + + std::copy(RAW_DATA_POINTS_1015, RAW_DATA_POINTS_1015 + 13, this->raw_data_points_); + std::copy(MPS_DATA_POINTS_1015, MPS_DATA_POINTS_1015 + 13, this->mps_data_points_); + } +} + +void FS3000Component::update() { + // 5 bytes of data read from fs3000 sensor + // byte 1 - checksum + // byte 2 - (lower 4 bits) high byte of sensor reading + // byte 3 - (8 bits) low byte of sensor reading + // byte 4 - generic checksum data + // byte 5 - generic checksum data + + uint8_t data[5]; + + if (!this->read_bytes_raw(data, 5)) { + this->status_set_warning(); + ESP_LOGW(TAG, "Error reading data from FS3000"); + this->publish_state(NAN); + return; + } + + // checksum passes if the modulo 256 sum of the five bytes is 0 + uint8_t checksum = 0; + for (uint8_t i : data) { + checksum += i; + } + + if (checksum != 0) { + this->status_set_warning(); + ESP_LOGW(TAG, "Checksum failure when reading from FS3000"); + return; + } + + // raw value information is 12 bits + uint16_t raw_value = (data[1] << 8) | data[2]; + ESP_LOGV(TAG, "Got raw reading=%i", raw_value); + + // convert and publish the raw value into m/s using the table of data points in the datasheet + this->publish_state(fit_raw_(raw_value)); + + this->status_clear_warning(); +} + +void FS3000Component::dump_config() { + ESP_LOGCONFIG(TAG, "FS3000:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Air Velocity", this); +} + +float FS3000Component::fit_raw_(uint16_t raw_value) { + // converts a raw value read from the FS3000 into a speed in m/s based on the + // reference data points given in the datasheet + // fits raw reading using a linear interpolation between each data point + + uint8_t end = 8; // assume model 1005, which has 9 data points + if (this->model_ == FIFTEEN) + end = 12; // model 1015 has 13 data points + + if (raw_value <= this->raw_data_points_[0]) { // less than smallest data point returns first data point + return this->mps_data_points_[0]; + } else if (raw_value >= this->raw_data_points_[end]) { // greater than largest data point returns max speed + return this->mps_data_points_[end]; + } else { + uint8_t i = 0; + + // determine between which data points does the reading fall, i-1 and i + while (raw_value > this->raw_data_points_[i]) { + ++i; + } + + // calculate the slope of the secant line between the two data points that surrounds the reading + float slope = (this->mps_data_points_[i] - this->mps_data_points_[i - 1]) / + (this->raw_data_points_[i] - this->raw_data_points_[i - 1]); + + // return the interpolated value for the reading + return (float(raw_value - this->raw_data_points_[i - 1])) * slope + this->mps_data_points_[i - 1]; + } +} + +} // namespace fs3000 +} // namespace esphome diff --git a/esphome/components/fs3000/fs3000.h b/esphome/components/fs3000/fs3000.h new file mode 100644 index 0000000000..be3680e7e1 --- /dev/null +++ b/esphome/components/fs3000/fs3000.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace fs3000 { + +// FS3000 has two models, 1005 and 1015 +// 1005 has a max speed detection of 7.23 m/s +// 1015 has a max speed detection of 15 m/s +enum FS3000Model { FIVE, FIFTEEN }; + +class FS3000Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { + public: + void setup() override; + void update() override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_model(FS3000Model model) { this->model_ = model; } + + protected: + FS3000Model model_{}; + + uint16_t raw_data_points_[13]; + float mps_data_points_[13]; + + float fit_raw_(uint16_t raw_value); +}; + +} // namespace fs3000 +} // namespace esphome diff --git a/esphome/components/fs3000/sensor.py b/esphome/components/fs3000/sensor.py new file mode 100644 index 0000000000..0c50f52979 --- /dev/null +++ b/esphome/components/fs3000/sensor.py @@ -0,0 +1,50 @@ +# initially based off of TMP117 component + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_MODEL, + DEVICE_CLASS_WIND_SPEED, + STATE_CLASS_MEASUREMENT, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@kahrendt"] + +fs3000_ns = cg.esphome_ns.namespace("fs3000") + +FS3000Model = fs3000_ns.enum("MODEL") +FS3000_MODEL_OPTIONS = { + "1005": FS3000Model.FIVE, + "1015": FS3000Model.FIFTEEN, +} + +FS3000Component = fs3000_ns.class_( + "FS3000Component", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + FS3000Component, + unit_of_measurement="m/s", + accuracy_decimals=2, + device_class=DEVICE_CLASS_WIND_SPEED, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.Required(CONF_MODEL): cv.enum(FS3000_MODEL_OPTIONS, lower=True), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x28)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp index 291af8c8cd..6c7adebfea 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.cpp +++ b/esphome/components/fujitsu_general/fujitsu_general.cpp @@ -151,11 +151,13 @@ void FujitsuGeneralClimate::transmit_state() { case climate::CLIMATE_FAN_LOW: SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_LOW); break; + case climate::CLIMATE_FAN_QUIET: + SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_SILENT); + break; case climate::CLIMATE_FAN_AUTO: default: SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_AUTO); break; - // TODO Quiet / Silent } // Set swing @@ -345,8 +347,9 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) { const uint8_t recv_fan_mode = GET_NIBBLE(recv_message, FUJITSU_GENERAL_FAN_NIBBLE); ESP_LOGV(TAG, "Received fan mode %X", recv_fan_mode); switch (recv_fan_mode) { - // TODO No Quiet / Silent in ESPH case FUJITSU_GENERAL_FAN_SILENT: + this->fan_mode = climate::CLIMATE_FAN_QUIET; + break; case FUJITSU_GENERAL_FAN_LOW: this->fan_mode = climate::CLIMATE_FAN_LOW; break; diff --git a/esphome/components/fujitsu_general/fujitsu_general.h b/esphome/components/fujitsu_general/fujitsu_general.h index 8dc7a3e484..d7d01bf6f3 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.h +++ b/esphome/components/fujitsu_general/fujitsu_general.h @@ -17,29 +17,29 @@ const uint8_t FUJITSU_GENERAL_TEMP_MAX = 30; // Celsius * turn * on temp mode fan swing * * | | | | | | * - * + * * temperatures 1 1248 124 124 1 * auto auto 18 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000100 00000000 00000000 00000000 00000000 00000000 00000100 11110001 * auto auto 19 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10001100 00000000 00000000 00000000 00000000 00000000 00000100 11111110 * auto auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00000000 00000000 00000000 00000000 00000000 00000100 11110011 - * + * * on flag: * on at 16 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000000 00100000 00000000 00000000 00000000 00000000 00000100 11010101 * down to 16 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000000 00100000 00000000 00000000 00000000 00000000 00000100 00110101 - * + * * mode options: * auto auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00000000 00000000 00000000 00000000 00000000 00000100 11110011 * cool auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 10000000 00000000 00000000 00000000 00000000 00000100 01110011 * dry auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 01000000 00000000 00000000 00000000 00000000 00000100 10110011 * fan (auto) (30) 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 11000000 00000000 00000000 00000000 00000000 00000100 00110011 * heat auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00100000 00000000 00000000 00000000 00000000 00000100 11010011 - * + * * fan options: * heat 30 high 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00100000 10000000 00000000 00000000 00000000 00000100 01010011 * heat 30 med 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 01000000 00000000 00000000 00000000 00000100 01010011 * heat 30 low 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 11000000 00000000 00000000 00000000 00000100 10010011 * heat 30 quiet 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00100000 00000000 00000000 00000000 00000100 00010011 - * + * * swing options: * heat 30 swing vert 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00101000 00000000 00000000 00000000 00000100 00011101 * heat 30 noswing 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00100000 00000000 00000000 00000000 00000100 00010011 @@ -52,7 +52,7 @@ class FujitsuGeneralClimate : public climate_ir::ClimateIR { FujitsuGeneralClimate() : ClimateIR(FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX, 1.0f, true, true, {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, - climate::CLIMATE_FAN_HIGH}, + climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_QUIET}, {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {} diff --git a/esphome/components/globals/globals_component.h b/esphome/components/globals/globals_component.h index 3286e43575..101adeb311 100644 --- a/esphome/components/globals/globals_component.h +++ b/esphome/components/globals/globals_component.h @@ -44,7 +44,14 @@ template class RestoringGlobalsComponent : public Component { float get_setup_priority() const override { return setup_priority::HARDWARE; } - void loop() override { + void loop() override { store_value_(); } + + void on_shutdown() override { store_value_(); } + + void set_name_hash(uint32_t name_hash) { this->name_hash_ = name_hash; } + + protected: + void store_value_() { int diff = memcmp(&this->value_, &this->prev_value_, sizeof(T)); if (diff != 0) { this->rtc_.save(&this->value_); @@ -52,9 +59,6 @@ template class RestoringGlobalsComponent : public Component { } } - void set_name_hash(uint32_t name_hash) { this->name_hash_ = name_hash; } - - protected: T value_{}; T prev_value_{}; uint32_t name_hash_{}; diff --git a/esphome/components/gp8403/__init__.py b/esphome/components/gp8403/__init__.py new file mode 100644 index 0000000000..a7a2b46f58 --- /dev/null +++ b/esphome/components/gp8403/__init__.py @@ -0,0 +1,40 @@ +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome.components import i2c +from esphome.const import CONF_ID, CONF_VOLTAGE + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +gp8403_ns = cg.esphome_ns.namespace("gp8403") +GP8403 = gp8403_ns.class_("GP8403", cg.Component, i2c.I2CDevice) + +GP8403Voltage = gp8403_ns.enum("GP8403Voltage") + +CONF_GP8403_ID = "gp8403_id" + +VOLTAGES = { + "5V": GP8403Voltage.GP8403_VOLTAGE_5V, + "10V": GP8403Voltage.GP8403_VOLTAGE_10V, +} + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(GP8403), + cv.Required(CONF_VOLTAGE): cv.enum(VOLTAGES, upper=True), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x58)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + cg.add(var.set_voltage(config[CONF_VOLTAGE])) diff --git a/esphome/components/gp8403/gp8403.cpp b/esphome/components/gp8403/gp8403.cpp new file mode 100644 index 0000000000..7a08a18a8f --- /dev/null +++ b/esphome/components/gp8403/gp8403.cpp @@ -0,0 +1,21 @@ +#include "gp8403.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace gp8403 { + +static const char *const TAG = "gp8403"; + +static const uint8_t RANGE_REGISTER = 0x01; + +void GP8403::setup() { this->write_register(RANGE_REGISTER, (uint8_t *) (&this->voltage_), 1); } + +void GP8403::dump_config() { + ESP_LOGCONFIG(TAG, "GP8403:"); + ESP_LOGCONFIG(TAG, " Voltage: %dV", this->voltage_ == GP8403_VOLTAGE_5V ? 5 : 10); + LOG_I2C_DEVICE(this); +} + +} // namespace gp8403 +} // namespace esphome diff --git a/esphome/components/gp8403/gp8403.h b/esphome/components/gp8403/gp8403.h new file mode 100644 index 0000000000..65182ef301 --- /dev/null +++ b/esphome/components/gp8403/gp8403.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace gp8403 { + +enum GP8403Voltage { + GP8403_VOLTAGE_5V = 0x00, + GP8403_VOLTAGE_10V = 0x11, +}; + +class GP8403 : public Component, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_voltage(gp8403::GP8403Voltage voltage) { this->voltage_ = voltage; } + + protected: + GP8403Voltage voltage_; +}; + +} // namespace gp8403 +} // namespace esphome diff --git a/esphome/components/gp8403/output/__init__.py b/esphome/components/gp8403/output/__init__.py new file mode 100644 index 0000000000..1cf95ac6e5 --- /dev/null +++ b/esphome/components/gp8403/output/__init__.py @@ -0,0 +1,31 @@ +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome.components import i2c, output +from esphome.const import CONF_ID, CONF_CHANNEL + +from .. import gp8403_ns, GP8403, CONF_GP8403_ID + +DEPENDENCIES = ["gp8403"] + +GP8403Output = gp8403_ns.class_( + "GP8403Output", cg.Component, i2c.I2CDevice, output.FloatOutput +) + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(GP8403Output), + cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403), + cv.Required(CONF_CHANNEL): cv.one_of(0, 1), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await output.register_output(var, config) + + await cg.register_parented(var, config[CONF_GP8403_ID]) + + cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/gp8403/output/gp8403_output.cpp b/esphome/components/gp8403/output/gp8403_output.cpp new file mode 100644 index 0000000000..ff73bb4627 --- /dev/null +++ b/esphome/components/gp8403/output/gp8403_output.cpp @@ -0,0 +1,26 @@ +#include "gp8403_output.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace gp8403 { + +static const char *const TAG = "gp8403.output"; + +static const uint8_t OUTPUT_REGISTER = 0x02; + +void GP8403Output::dump_config() { + ESP_LOGCONFIG(TAG, "GP8403 Output:"); + ESP_LOGCONFIG(TAG, " Channel: %u", this->channel_); +} + +void GP8403Output::write_state(float state) { + uint16_t value = ((uint16_t) (state * 4095)) << 4; + i2c::ErrorCode err = this->parent_->write_register(OUTPUT_REGISTER + (2 * this->channel_), (uint8_t *) &value, 2); + if (err != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Error writing to GP8403, code %d", err); + } +} + +} // namespace gp8403 +} // namespace esphome diff --git a/esphome/components/gp8403/output/gp8403_output.h b/esphome/components/gp8403/output/gp8403_output.h new file mode 100644 index 0000000000..71e5efb1cb --- /dev/null +++ b/esphome/components/gp8403/output/gp8403_output.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/components/output/float_output.h" +#include "esphome/core/component.h" + +#include "esphome/components/gp8403/gp8403.h" + +namespace esphome { +namespace gp8403 { + +class GP8403Output : public Component, public output::FloatOutput, public Parented { + public: + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA - 1; } + + void set_channel(uint8_t channel) { this->channel_ = channel; } + + void write_state(float state) override; + + protected: + uint8_t channel_; +}; + +} // namespace gp8403 +} // namespace esphome diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 4d91b81a44..786c3f4b96 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -2,25 +2,27 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import binary_sensor -from esphome.const import CONF_ID, CONF_PIN +from esphome.const import CONF_PIN from .. import gpio_ns GPIOBinarySensor = gpio_ns.class_( "GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component ) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(GPIOBinarySensor), - cv.Required(CONF_PIN): pins.gpio_input_pin_schema, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + binary_sensor.binary_sensor_schema(GPIOBinarySensor) + .extend( + { + cv.Required(CONF_PIN): pins.gpio_input_pin_schema, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) - await binary_sensor.register_binary_sensor(var, config) pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) diff --git a/esphome/components/gpio/switch/__init__.py b/esphome/components/gpio/switch/__init__.py index a03e16a2c1..9da6870a46 100644 --- a/esphome/components/gpio/switch/__init__.py +++ b/esphome/components/gpio/switch/__init__.py @@ -2,47 +2,34 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import switch -from esphome.const import CONF_ID, CONF_INTERLOCK, CONF_PIN, CONF_RESTORE_MODE +from esphome.const import CONF_INTERLOCK, CONF_PIN from .. import gpio_ns GPIOSwitch = gpio_ns.class_("GPIOSwitch", switch.Switch, cg.Component) -GPIOSwitchRestoreMode = gpio_ns.enum("GPIOSwitchRestoreMode") - -RESTORE_MODES = { - "RESTORE_DEFAULT_OFF": GPIOSwitchRestoreMode.GPIO_SWITCH_RESTORE_DEFAULT_OFF, - "RESTORE_DEFAULT_ON": GPIOSwitchRestoreMode.GPIO_SWITCH_RESTORE_DEFAULT_ON, - "ALWAYS_OFF": GPIOSwitchRestoreMode.GPIO_SWITCH_ALWAYS_OFF, - "ALWAYS_ON": GPIOSwitchRestoreMode.GPIO_SWITCH_ALWAYS_ON, - "RESTORE_INVERTED_DEFAULT_OFF": GPIOSwitchRestoreMode.GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_OFF, - "RESTORE_INVERTED_DEFAULT_ON": GPIOSwitchRestoreMode.GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_ON, -} CONF_INTERLOCK_WAIT_TIME = "interlock_wait_time" -CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(GPIOSwitch), - cv.Required(CONF_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_RESTORE_MODE, default="RESTORE_DEFAULT_OFF"): cv.enum( - RESTORE_MODES, upper=True, space="_" - ), - cv.Optional(CONF_INTERLOCK): cv.ensure_list(cv.use_id(switch.Switch)), - cv.Optional( - CONF_INTERLOCK_WAIT_TIME, default="0ms" - ): cv.positive_time_period_milliseconds, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + switch.switch_schema(GPIOSwitch) + .extend( + { + cv.Required(CONF_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_INTERLOCK): cv.ensure_list(cv.use_id(switch.Switch)), + cv.Optional( + CONF_INTERLOCK_WAIT_TIME, default="0ms" + ): cv.positive_time_period_milliseconds, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await switch.new_switch(config) await cg.register_component(var, config) - await switch.register_switch(var, config) pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) - cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) - if CONF_INTERLOCK in config: interlock = [] for it in config[CONF_INTERLOCK]: diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp index 714f2ea6d8..5033315b5e 100644 --- a/esphome/components/gpio/switch/gpio_switch.cpp +++ b/esphome/components/gpio/switch/gpio_switch.cpp @@ -10,27 +10,7 @@ float GPIOSwitch::get_setup_priority() const { return setup_priority::HARDWARE; void GPIOSwitch::setup() { ESP_LOGCONFIG(TAG, "Setting up GPIO Switch '%s'...", this->name_.c_str()); - bool initial_state = false; - switch (this->restore_mode_) { - case GPIO_SWITCH_RESTORE_DEFAULT_OFF: - initial_state = this->get_initial_state().value_or(false); - break; - case GPIO_SWITCH_RESTORE_DEFAULT_ON: - initial_state = this->get_initial_state().value_or(true); - break; - case GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_OFF: - initial_state = !this->get_initial_state().value_or(true); - break; - case GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_ON: - initial_state = !this->get_initial_state().value_or(false); - break; - case GPIO_SWITCH_ALWAYS_OFF: - initial_state = false; - break; - case GPIO_SWITCH_ALWAYS_ON: - initial_state = true; - break; - } + bool initial_state = this->get_initial_state_with_restore_mode().value_or(false); // write state before setup if (initial_state) { @@ -49,28 +29,6 @@ void GPIOSwitch::setup() { void GPIOSwitch::dump_config() { LOG_SWITCH("", "GPIO Switch", this); LOG_PIN(" Pin: ", this->pin_); - const LogString *restore_mode = LOG_STR(""); - switch (this->restore_mode_) { - case GPIO_SWITCH_RESTORE_DEFAULT_OFF: - restore_mode = LOG_STR("Restore (Defaults to OFF)"); - break; - case GPIO_SWITCH_RESTORE_DEFAULT_ON: - restore_mode = LOG_STR("Restore (Defaults to ON)"); - break; - case GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_ON: - restore_mode = LOG_STR("Restore inverted (Defaults to ON)"); - break; - case GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_OFF: - restore_mode = LOG_STR("Restore inverted (Defaults to OFF)"); - break; - case GPIO_SWITCH_ALWAYS_OFF: - restore_mode = LOG_STR("Always OFF"); - break; - case GPIO_SWITCH_ALWAYS_ON: - restore_mode = LOG_STR("Always ON"); - break; - } - ESP_LOGCONFIG(TAG, " Restore Mode: %s", LOG_STR_ARG(restore_mode)); if (!this->interlock_.empty()) { ESP_LOGCONFIG(TAG, " Interlocks:"); for (auto *lock : this->interlock_) { @@ -111,7 +69,6 @@ void GPIOSwitch::write_state(bool state) { this->pin_->digital_write(state); this->publish_state(state); } -void GPIOSwitch::set_restore_mode(GPIOSwitchRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } void GPIOSwitch::set_interlock(const std::vector &interlock) { this->interlock_ = interlock; } } // namespace gpio diff --git a/esphome/components/gpio/switch/gpio_switch.h b/esphome/components/gpio/switch/gpio_switch.h index 99f8060efa..94d49745b5 100644 --- a/esphome/components/gpio/switch/gpio_switch.h +++ b/esphome/components/gpio/switch/gpio_switch.h @@ -4,24 +4,15 @@ #include "esphome/core/hal.h" #include "esphome/components/switch/switch.h" +#include + namespace esphome { namespace gpio { -enum GPIOSwitchRestoreMode { - GPIO_SWITCH_RESTORE_DEFAULT_OFF, - GPIO_SWITCH_RESTORE_DEFAULT_ON, - GPIO_SWITCH_ALWAYS_OFF, - GPIO_SWITCH_ALWAYS_ON, - GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_OFF, - GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_ON, -}; - class GPIOSwitch : public switch_::Switch, public Component { public: void set_pin(GPIOPin *pin) { pin_ = pin; } - void set_restore_mode(GPIOSwitchRestoreMode restore_mode); - // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) float get_setup_priority() const override; @@ -35,7 +26,6 @@ class GPIOSwitch : public switch_::Switch, public Component { void write_state(bool state) override; GPIOPin *pin_; - GPIOSwitchRestoreMode restore_mode_{GPIO_SWITCH_RESTORE_DEFAULT_OFF}; std::vector interlock_; uint32_t interlock_wait_time_{0}; }; diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index e485373175..d4cf79b49e 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -11,7 +11,6 @@ from esphome.const import ( CONF_ALTITUDE, CONF_SATELLITES, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, UNIT_DEGREES, UNIT_KILOMETER_PER_HOUR, UNIT_METER, @@ -35,27 +34,22 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_LATITUDE): sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, accuracy_decimals=6, - state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_LONGITUDE): sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, accuracy_decimals=6, - state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_SPEED): sensor.sensor_schema( unit_of_measurement=UNIT_KILOMETER_PER_HOUR, accuracy_decimals=6, - state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_COURSE): sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, accuracy_decimals=2, - state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_ALTITUDE): sensor.sensor_schema( unit_of_measurement=UNIT_METER, accuracy_decimals=1, - state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_SATELLITES): sensor.sensor_schema( accuracy_decimals=0, diff --git a/esphome/components/gps/gps.h b/esphome/components/gps/gps.h index 40cda145ca..0626fb0b0e 100644 --- a/esphome/components/gps/gps.h +++ b/esphome/components/gps/gps.h @@ -7,6 +7,8 @@ #include "esphome/components/sensor/sensor.h" #include +#include + namespace esphome { namespace gps { diff --git a/esphome/components/gps/time/gps_time.cpp b/esphome/components/gps/time/gps_time.cpp index e46f24ba8e..0f1b989f77 100644 --- a/esphome/components/gps/time/gps_time.cpp +++ b/esphome/components/gps/time/gps_time.cpp @@ -16,7 +16,7 @@ void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { if (tiny_gps.date.year() < 2019) return; - time::ESPTime val{}; + ESPTime val{}; val.year = tiny_gps.date.year(); val.month = tiny_gps.date.month(); val.day_of_month = tiny_gps.date.day(); diff --git a/esphome/components/graph/__init__.py b/esphome/components/graph/__init__.py index 12acfee869..046f59ca1a 100644 --- a/esphome/components/graph/__init__.py +++ b/esphome/components/graph/__init__.py @@ -118,7 +118,7 @@ def _relocate_fields_to_subfolder(config, subfolder, subschema): fields = [k.schema for k in subschema.schema.keys()] fields.remove(CONF_ID) if subfolder in config: - # Ensure no ambigious fields in base of config + # Ensure no ambiguous fields in base of config for f in fields: if f in config: raise cv.Invalid( diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index daff89e0a6..c229f17dd8 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -122,11 +122,18 @@ void Graph::draw(DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Colo } // Adjust limits to nice y_per_div boundaries - int yn = int(ymin / y_per_div); - int ym = int(ymax / y_per_div) + int(1 * (fmodf(ymax, y_per_div) != 0)); - ymin = yn * y_per_div; - ymax = ym * y_per_div; - yrange = ymax - ymin; + int yn = 0; + int ym = 1; + if (!std::isnan(ymin) && !std::isnan(ymax)) { + yn = (int) floorf(ymin / y_per_div); + ym = (int) ceilf(ymax / y_per_div); + if (yn == ym) { + ym++; + } + ymin = yn * y_per_div; + ymax = ym * y_per_div; + yrange = ymax - ymin; + } /// Draw grid if (!std::isnan(this->gridspacing_y_)) { diff --git a/esphome/components/graph/graph.h b/esphome/components/graph/graph.h index 15d2d1c7c4..87c21fd7d1 100644 --- a/esphome/components/graph/graph.h +++ b/esphome/components/graph/graph.h @@ -1,16 +1,17 @@ #pragma once +#include +#include +#include #include "esphome/components/sensor/sensor.h" #include "esphome/core/color.h" #include "esphome/core/component.h" -#include -#include namespace esphome { // forward declare DisplayBuffer namespace display { class DisplayBuffer; -class Font; +class BaseFont; } // namespace display namespace graph { @@ -44,8 +45,8 @@ enum ValuePositionType { class GraphLegend { public: void init(Graph *g); - void set_name_font(display::Font *font) { this->font_label_ = font; } - void set_value_font(display::Font *font) { this->font_value_ = font; } + void set_name_font(display::BaseFont *font) { this->font_label_ = font; } + void set_value_font(display::BaseFont *font) { this->font_value_ = font; } void set_width(uint32_t width) { this->width_ = width; } void set_height(uint32_t height) { this->height_ = height; } void set_border(bool val) { this->border_ = val; } @@ -62,8 +63,8 @@ class GraphLegend { ValuePositionType values_{VALUE_POSITION_TYPE_AUTO}; bool units_{true}; DirectionType direction_{DIRECTION_TYPE_AUTO}; - display::Font *font_label_{nullptr}; - display::Font *font_value_{nullptr}; + display::BaseFont *font_label_{nullptr}; + display::BaseFont *font_value_{nullptr}; // Calculated values Graph *parent_{nullptr}; // (x0) (xs,ys) (xs,ys) diff --git a/esphome/components/growatt_solar/growatt_solar.cpp b/esphome/components/growatt_solar/growatt_solar.cpp index ed7240ab6c..c4ed5ab841 100644 --- a/esphome/components/growatt_solar/growatt_solar.cpp +++ b/esphome/components/growatt_solar/growatt_solar.cpp @@ -7,11 +7,44 @@ namespace growatt_solar { static const char *const TAG = "growatt_solar"; static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04; -static const uint8_t MODBUS_REGISTER_COUNT = 33; +static const uint8_t MODBUS_REGISTER_COUNT[] = {33, 95}; // indexed with enum GrowattProtocolVersion -void GrowattSolar::update() { this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT); } +void GrowattSolar::loop() { + // If update() was unable to send we retry until we can send. + if (!this->waiting_to_update_) + return; + update(); +} + +void GrowattSolar::update() { + // If our last send has had no reply yet, and it wasn't that long ago, do nothing. + uint32_t now = millis(); + if (now - this->last_send_ < this->get_update_interval() / 2) { + return; + } + + // The bus might be slow, or there might be other devices, or other components might be talking to our device. + if (this->waiting_for_response()) { + this->waiting_to_update_ = true; + return; + } + + this->waiting_to_update_ = false; + this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT[this->protocol_version_]); + this->last_send_ = millis(); +} void GrowattSolar::on_modbus_data(const std::vector &data) { + // Other components might be sending commands to our device. But we don't get called with enough + // context to know what is what. So if we didn't do a send, we ignore the data. + if (!this->last_send_) + return; + this->last_send_ = 0; + + // Also ignore the data if the message is too short. Otherwise we will publish invalid values. + if (data.size() < MODBUS_REGISTER_COUNT[this->protocol_version_] * 2) + return; + auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void { if (sensor == nullptr) return; @@ -27,37 +60,76 @@ void GrowattSolar::on_modbus_data(const std::vector &data) { sensor->publish_state(value); }; - publish_1_reg_sensor_state(this->inverter_status_, 0, 1); + switch (this->protocol_version_) { + case RTU: { + publish_1_reg_sensor_state(this->inverter_status_, 0, 1); - publish_2_reg_sensor_state(this->pv_active_power_sensor_, 1, 2, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pv_active_power_sensor_, 1, 2, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, 3, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, 4, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, 5, 6, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, 3, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, 4, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, 5, 6, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, 7, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, 8, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, 9, 10, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, 7, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, 8, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, 9, 10, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->grid_active_power_sensor_, 11, 12, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->grid_frequency_sensor_, 13, TWO_DEC_UNIT); + publish_2_reg_sensor_state(this->grid_active_power_sensor_, 11, 12, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->grid_frequency_sensor_, 13, TWO_DEC_UNIT); - publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, 14, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->phases_[0].current_sensor_, 15, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, 16, 17, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, 14, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[0].current_sensor_, 15, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, 16, 17, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, 18, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->phases_[1].current_sensor_, 19, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, 20, 21, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, 18, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[1].current_sensor_, 19, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, 20, 21, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, 22, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->phases_[2].current_sensor_, 23, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, 24, 25, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, 22, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[2].current_sensor_, 23, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, 24, 25, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->today_production_, 26, 27, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->total_energy_production_, 28, 29, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->today_production_, 26, 27, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->total_energy_production_, 28, 29, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->inverter_module_temp_, 32, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->inverter_module_temp_, 32, ONE_DEC_UNIT); + break; + } + case RTU2: { + publish_1_reg_sensor_state(this->inverter_status_, 0, 1); + + publish_2_reg_sensor_state(this->pv_active_power_sensor_, 1, 2, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, 3, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, 4, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, 5, 6, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, 7, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, 8, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, 9, 10, ONE_DEC_UNIT); + + publish_2_reg_sensor_state(this->grid_active_power_sensor_, 35, 36, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->grid_frequency_sensor_, 37, TWO_DEC_UNIT); + + publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, 38, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[0].current_sensor_, 39, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, 40, 41, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, 42, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[1].current_sensor_, 43, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, 44, 45, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, 46, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[2].current_sensor_, 47, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, 48, 49, ONE_DEC_UNIT); + + publish_2_reg_sensor_state(this->today_production_, 53, 54, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->total_energy_production_, 55, 56, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->inverter_module_temp_, 93, ONE_DEC_UNIT); + break; + } + } } void GrowattSolar::dump_config() { diff --git a/esphome/components/growatt_solar/growatt_solar.h b/esphome/components/growatt_solar/growatt_solar.h index 5356ac907a..b0ddd4b99d 100644 --- a/esphome/components/growatt_solar/growatt_solar.h +++ b/esphome/components/growatt_solar/growatt_solar.h @@ -4,18 +4,28 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/modbus/modbus.h" +#include + namespace esphome { namespace growatt_solar { static const float TWO_DEC_UNIT = 0.01; static const float ONE_DEC_UNIT = 0.1; +enum GrowattProtocolVersion { + RTU = 0, + RTU2, +}; + class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { public: + void loop() override; void update() override; void on_modbus_data(const std::vector &data) override; void dump_config() override; + void set_protocol_version(GrowattProtocolVersion protocol_version) { this->protocol_version_ = protocol_version; } + void set_inverter_status_sensor(sensor::Sensor *sensor) { this->inverter_status_ = sensor; } void set_grid_frequency_sensor(sensor::Sensor *sensor) { this->grid_frequency_sensor_ = sensor; } @@ -46,6 +56,9 @@ class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { } protected: + bool waiting_to_update_; + uint32_t last_send_; + struct GrowattPhase { sensor::Sensor *voltage_sensor_{nullptr}; sensor::Sensor *current_sensor_{nullptr}; @@ -67,6 +80,7 @@ class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { sensor::Sensor *today_production_{nullptr}; sensor::Sensor *total_energy_production_{nullptr}; sensor::Sensor *inverter_module_temp_{nullptr}; + GrowattProtocolVersion protocol_version_; }; } // namespace growatt_solar diff --git a/esphome/components/growatt_solar/sensor.py b/esphome/components/growatt_solar/sensor.py index 99936c33ee..f95d679c3e 100644 --- a/esphome/components/growatt_solar/sensor.py +++ b/esphome/components/growatt_solar/sensor.py @@ -39,7 +39,7 @@ UNIT_MILLIAMPERE = "mA" CONF_INVERTER_STATUS = "inverter_status" CONF_PV_ACTIVE_POWER = "pv_active_power" CONF_INVERTER_MODULE_TEMP = "inverter_module_temp" - +CONF_PROTOCOL_VERSION = "protocol_version" AUTO_LOAD = ["modbus"] CODEOWNERS = ["@leeuwte"] @@ -52,7 +52,7 @@ GrowattSolar = growatt_solar_ns.class_( PHASE_SENSORS = { CONF_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, - accuracy_decimals=2, + accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, ), CONF_CURRENT: sensor.sensor_schema( @@ -71,7 +71,7 @@ PHASE_SENSORS = { PV_SENSORS = { CONF_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, - accuracy_decimals=2, + accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, ), CONF_CURRENT: sensor.sensor_schema( @@ -95,10 +95,20 @@ PV_SCHEMA = cv.Schema( {cv.Optional(sensor): schema for sensor, schema in PV_SENSORS.items()} ) +GrowattProtocolVersion = growatt_solar_ns.enum("GrowattProtocolVersion") +PROTOCOL_VERSIONS = { + "RTU": GrowattProtocolVersion.RTU, + "RTU2": GrowattProtocolVersion.RTU2, +} + + CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(GrowattSolar), + cv.Optional(CONF_PROTOCOL_VERSION, default="RTU"): cv.enum( + PROTOCOL_VERSIONS, upper=True + ), cv.Optional(CONF_PHASE_A): PHASE_SCHEMA, cv.Optional(CONF_PHASE_B): PHASE_SCHEMA, cv.Optional(CONF_PHASE_C): PHASE_SCHEMA, @@ -125,13 +135,13 @@ CONFIG_SCHEMA = ( ), cv.Optional(CONF_ENERGY_PRODUCTION_DAY): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT_HOURS, - accuracy_decimals=2, + accuracy_decimals=1, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_TOTAL_ENERGY_PRODUCTION): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT_HOURS, - accuracy_decimals=0, + accuracy_decimals=1, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), @@ -152,6 +162,8 @@ async def to_code(config): await cg.register_component(var, config) await modbus.register_modbus_device(var, config) + cg.add(var.set_protocol_version(config[CONF_PROTOCOL_VERSION])) + if CONF_INVERTER_STATUS in config: sens = await sensor.new_sensor(config[CONF_INVERTER_STATUS]) cg.add(var.set_inverter_status_sensor(sens)) diff --git a/esphome/components/haier/__init__.py b/esphome/components/haier/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/haier/automation.h b/esphome/components/haier/automation.h new file mode 100644 index 0000000000..84e4554db8 --- /dev/null +++ b/esphome/components/haier/automation.h @@ -0,0 +1,130 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "haier_base.h" +#include "hon_climate.h" + +namespace esphome { +namespace haier { + +template class DisplayOnAction : public Action { + public: + DisplayOnAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_display_state(true); } + + protected: + HaierClimateBase *parent_; +}; + +template class DisplayOffAction : public Action { + public: + DisplayOffAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_display_state(false); } + + protected: + HaierClimateBase *parent_; +}; + +template class BeeperOnAction : public Action { + public: + BeeperOnAction(HonClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_beeper_state(true); } + + protected: + HonClimate *parent_; +}; + +template class BeeperOffAction : public Action { + public: + BeeperOffAction(HonClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_beeper_state(false); } + + protected: + HonClimate *parent_; +}; + +template class VerticalAirflowAction : public Action { + public: + VerticalAirflowAction(HonClimate *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(AirflowVerticalDirection, direction) + void play(Ts... x) { this->parent_->set_vertical_airflow(this->direction_.value(x...)); } + + protected: + HonClimate *parent_; +}; + +template class HorizontalAirflowAction : public Action { + public: + HorizontalAirflowAction(HonClimate *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(AirflowHorizontalDirection, direction) + void play(Ts... x) { this->parent_->set_horizontal_airflow(this->direction_.value(x...)); } + + protected: + HonClimate *parent_; +}; + +template class HealthOnAction : public Action { + public: + HealthOnAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_health_mode(true); } + + protected: + HaierClimateBase *parent_; +}; + +template class HealthOffAction : public Action { + public: + HealthOffAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_health_mode(false); } + + protected: + HaierClimateBase *parent_; +}; + +template class StartSelfCleaningAction : public Action { + public: + StartSelfCleaningAction(HonClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->start_self_cleaning(); } + + protected: + HonClimate *parent_; +}; + +template class StartSteriCleaningAction : public Action { + public: + StartSteriCleaningAction(HonClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->start_steri_cleaning(); } + + protected: + HonClimate *parent_; +}; + +template class PowerOnAction : public Action { + public: + PowerOnAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->send_power_on_command(); } + + protected: + HaierClimateBase *parent_; +}; + +template class PowerOffAction : public Action { + public: + PowerOffAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->send_power_off_command(); } + + protected: + HaierClimateBase *parent_; +}; + +template class PowerToggleAction : public Action { + public: + PowerToggleAction(HaierClimateBase *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->toggle_power(); } + + protected: + HaierClimateBase *parent_; +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py new file mode 100644 index 0000000000..12b76084ba --- /dev/null +++ b/esphome/components/haier/climate.py @@ -0,0 +1,364 @@ +import logging +import esphome.codegen as cg +import esphome.config_validation as cv +import esphome.final_validate as fv +from esphome.components import uart, sensor, climate, logger +from esphome import automation +from esphome.const import ( + CONF_BEEPER, + CONF_ID, + CONF_LEVEL, + CONF_LOGGER, + CONF_LOGS, + CONF_MAX_TEMPERATURE, + CONF_MIN_TEMPERATURE, + CONF_PROTOCOL, + CONF_SUPPORTED_MODES, + CONF_SUPPORTED_SWING_MODES, + CONF_VISUAL, + CONF_WIFI, + DEVICE_CLASS_TEMPERATURE, + ICON_THERMOMETER, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) +from esphome.components.climate import ( + ClimateSwingMode, + ClimateMode, +) + +_LOGGER = logging.getLogger(__name__) + +PROTOCOL_MIN_TEMPERATURE = 16.0 +PROTOCOL_MAX_TEMPERATURE = 30.0 +PROTOCOL_TEMPERATURE_STEP = 1.0 + +CODEOWNERS = ["@paveldn"] +AUTO_LOAD = ["sensor"] +DEPENDENCIES = ["climate", "uart"] +CONF_WIFI_SIGNAL = "wifi_signal" +CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" +CONF_VERTICAL_AIRFLOW = "vertical_airflow" +CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow" + + +PROTOCOL_HON = "HON" +PROTOCOL_SMARTAIR2 = "SMARTAIR2" +PROTOCOLS_SUPPORTED = [PROTOCOL_HON, PROTOCOL_SMARTAIR2] + +haier_ns = cg.esphome_ns.namespace("haier") +HaierClimateBase = haier_ns.class_( + "HaierClimateBase", uart.UARTDevice, climate.Climate, cg.Component +) +HonClimate = haier_ns.class_("HonClimate", HaierClimateBase) +Smartair2Climate = haier_ns.class_("Smartair2Climate", HaierClimateBase) + + +AirflowVerticalDirection = haier_ns.enum("AirflowVerticalDirection") +AIRFLOW_VERTICAL_DIRECTION_OPTIONS = { + "UP": AirflowVerticalDirection.UP, + "CENTER": AirflowVerticalDirection.CENTER, + "DOWN": AirflowVerticalDirection.DOWN, +} + +AirflowHorizontalDirection = haier_ns.enum("AirflowHorizontalDirection") +AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = { + "LEFT": AirflowHorizontalDirection.LEFT, + "CENTER": AirflowHorizontalDirection.CENTER, + "RIGHT": AirflowHorizontalDirection.RIGHT, +} + +SUPPORTED_SWING_MODES_OPTIONS = { + "OFF": ClimateSwingMode.CLIMATE_SWING_OFF, # always available + "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, # always available + "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, + "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH, +} + +SUPPORTED_CLIMATE_MODES_OPTIONS = { + "OFF": ClimateMode.CLIMATE_MODE_OFF, # always available + "AUTO": ClimateMode.CLIMATE_MODE_AUTO, # always available + "COOL": ClimateMode.CLIMATE_MODE_COOL, + "HEAT": ClimateMode.CLIMATE_MODE_HEAT, + "DRY": ClimateMode.CLIMATE_MODE_DRY, + "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY, +} + + +def validate_visual(config): + if CONF_VISUAL in config: + visual_config = config[CONF_VISUAL] + if CONF_MIN_TEMPERATURE in visual_config: + min_temp = visual_config[CONF_MIN_TEMPERATURE] + if min_temp < PROTOCOL_MIN_TEMPERATURE: + raise cv.Invalid( + f"Configured visual minimum temperature {min_temp} is lower than supported by Haier protocol is {PROTOCOL_MIN_TEMPERATURE}" + ) + else: + config[CONF_VISUAL][CONF_MIN_TEMPERATURE] = PROTOCOL_MIN_TEMPERATURE + if CONF_MAX_TEMPERATURE in visual_config: + max_temp = visual_config[CONF_MAX_TEMPERATURE] + if max_temp > PROTOCOL_MAX_TEMPERATURE: + raise cv.Invalid( + f"Configured visual maximum temperature {max_temp} is higher than supported by Haier protocol is {PROTOCOL_MAX_TEMPERATURE}" + ) + else: + config[CONF_VISUAL][CONF_MAX_TEMPERATURE] = PROTOCOL_MAX_TEMPERATURE + else: + config[CONF_VISUAL] = { + CONF_MIN_TEMPERATURE: PROTOCOL_MIN_TEMPERATURE, + CONF_MAX_TEMPERATURE: PROTOCOL_MAX_TEMPERATURE, + } + return config + + +BASE_CONFIG_SCHEMA = ( + climate.CLIMATE_SCHEMA.extend( + { + cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( + cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True) + ), + cv.Optional( + CONF_SUPPORTED_SWING_MODES, + default=[ + "OFF", + "VERTICAL", + "HORIZONTAL", + "BOTH", + ], + ): cv.ensure_list(cv.enum(SUPPORTED_SWING_MODES_OPTIONS, upper=True)), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + +CONFIG_SCHEMA = cv.All( + cv.typed_schema( + { + PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(Smartair2Climate), + } + ), + PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(HonClimate), + cv.Optional(CONF_WIFI_SIGNAL, default=True): cv.boolean, + cv.Optional(CONF_BEEPER, default=True): cv.boolean, + cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ), + }, + key=CONF_PROTOCOL, + default_type=PROTOCOL_SMARTAIR2, + upper=True, + ), + validate_visual, +) + + +# Actions +DisplayOnAction = haier_ns.class_("DisplayOnAction", automation.Action) +DisplayOffAction = haier_ns.class_("DisplayOffAction", automation.Action) +BeeperOnAction = haier_ns.class_("BeeperOnAction", automation.Action) +BeeperOffAction = haier_ns.class_("BeeperOffAction", automation.Action) +StartSelfCleaningAction = haier_ns.class_("StartSelfCleaningAction", automation.Action) +StartSteriCleaningAction = haier_ns.class_( + "StartSteriCleaningAction", automation.Action +) +VerticalAirflowAction = haier_ns.class_("VerticalAirflowAction", automation.Action) +HorizontalAirflowAction = haier_ns.class_("HorizontalAirflowAction", automation.Action) +HealthOnAction = haier_ns.class_("HealthOnAction", automation.Action) +HealthOffAction = haier_ns.class_("HealthOffAction", automation.Action) +PowerOnAction = haier_ns.class_("PowerOnAction", automation.Action) +PowerOffAction = haier_ns.class_("PowerOffAction", automation.Action) +PowerToggleAction = haier_ns.class_("PowerToggleAction", automation.Action) + +HAIER_BASE_ACTION_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(HaierClimateBase), + } +) + +HAIER_HON_BASE_ACTION_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(HonClimate), + } +) + + +@automation.register_action( + "climate.haier.display_on", DisplayOnAction, HAIER_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.display_off", DisplayOffAction, HAIER_BASE_ACTION_SCHEMA +) +async def display_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +@automation.register_action( + "climate.haier.beeper_on", BeeperOnAction, HAIER_HON_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.beeper_off", BeeperOffAction, HAIER_HON_BASE_ACTION_SCHEMA +) +async def beeper_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +# Start self cleaning or steri-cleaning action action +@automation.register_action( + "climate.haier.start_self_cleaning", + StartSelfCleaningAction, + HAIER_HON_BASE_ACTION_SCHEMA, +) +@automation.register_action( + "climate.haier.start_steri_cleaning", + StartSteriCleaningAction, + HAIER_HON_BASE_ACTION_SCHEMA, +) +async def start_cleaning_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +# Set vertical airflow direction action +@automation.register_action( + "climate.haier.set_vertical_airflow", + VerticalAirflowAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(HonClimate), + cv.Required(CONF_VERTICAL_AIRFLOW): cv.templatable( + cv.enum(AIRFLOW_VERTICAL_DIRECTION_OPTIONS, upper=True) + ), + } + ), +) +async def haier_set_vertical_airflow_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable( + config[CONF_VERTICAL_AIRFLOW], args, AirflowVerticalDirection + ) + cg.add(var.set_direction(template_)) + return var + + +# Set horizontal airflow direction action +@automation.register_action( + "climate.haier.set_horizontal_airflow", + HorizontalAirflowAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(HonClimate), + cv.Required(CONF_HORIZONTAL_AIRFLOW): cv.templatable( + cv.enum(AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS, upper=True) + ), + } + ), +) +async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable( + config[CONF_HORIZONTAL_AIRFLOW], args, AirflowHorizontalDirection + ) + cg.add(var.set_direction(template_)) + return var + + +@automation.register_action( + "climate.haier.health_on", HealthOnAction, HAIER_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.health_off", HealthOffAction, HAIER_BASE_ACTION_SCHEMA +) +async def health_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +@automation.register_action( + "climate.haier.power_on", PowerOnAction, HAIER_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.power_off", PowerOffAction, HAIER_BASE_ACTION_SCHEMA +) +@automation.register_action( + "climate.haier.power_toggle", PowerToggleAction, HAIER_BASE_ACTION_SCHEMA +) +async def power_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +def _final_validate(config): + full_config = fv.full_config.get() + if CONF_LOGGER in full_config: + _level = "NONE" + logger_config = full_config[CONF_LOGGER] + if CONF_LOGS in logger_config: + if "haier.protocol" in logger_config[CONF_LOGS]: + _level = logger_config[CONF_LOGS]["haier.protocol"] + else: + _level = logger_config[CONF_LEVEL] + _LOGGER.info("Detected log level for Haier protocol: %s", _level) + if _level not in logger.LOG_LEVEL_SEVERITY: + raise cv.Invalid("Unknown log level for Haier protocol") + _severity = logger.LOG_LEVEL_SEVERITY.index(_level) + cg.add_build_flag(f"-DHAIER_LOG_LEVEL={_severity}") + else: + _LOGGER.info( + "No logger component found, logging for Haier protocol is disabled" + ) + cg.add_build_flag("-DHAIER_LOG_LEVEL=0") + if ( + (CONF_WIFI_SIGNAL in config) + and (config[CONF_WIFI_SIGNAL]) + and CONF_WIFI not in full_config + ): + raise cv.Invalid( + f"No WiFi configured, if you want to use haier climate without WiFi add {CONF_WIFI_SIGNAL}: false to climate configuration" + ) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + cg.add(haier_ns.init_haier_protocol_logging()) + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + await climate.register_climate(var, config) + + if (CONF_WIFI_SIGNAL in config) and (config[CONF_WIFI_SIGNAL]): + cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL])) + if CONF_BEEPER in config: + cg.add(var.set_beeper_state(config[CONF_BEEPER])) + if CONF_OUTDOOR_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE]) + cg.add(var.set_outdoor_temperature_sensor(sens)) + if CONF_SUPPORTED_MODES in config: + cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) + if CONF_SUPPORTED_SWING_MODES in config: + cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES])) + # https://github.com/paveldn/HaierProtocol + cg.add_library("pavlodn/HaierProtocol", "0.9.18") diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp new file mode 100644 index 0000000000..d9349cb8fe --- /dev/null +++ b/esphome/components/haier/haier_base.cpp @@ -0,0 +1,311 @@ +#include +#include +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" +#include "haier_base.h" + +using namespace esphome::climate; +using namespace esphome::uart; + +namespace esphome { +namespace haier { + +static const char *const TAG = "haier.climate"; +constexpr size_t COMMUNICATION_TIMEOUT_MS = 60000; +constexpr size_t STATUS_REQUEST_INTERVAL_MS = 5000; +constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000; +constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000; +constexpr size_t CONTROL_MESSAGES_INTERVAL_MS = 400; +constexpr size_t CONTROL_TIMEOUT_MS = 7000; +constexpr size_t NO_COMMAND = 0xFF; // Indicate that there is no command supplied + +#if (HAIER_LOG_LEVEL > 4) +// To reduce size of binary this function only available when log level is Verbose +const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) { + static const char *phase_names[] = { + "SENDING_INIT_1", + "WAITING_ANSWER_INIT_1", + "SENDING_INIT_2", + "WAITING_ANSWER_INIT_2", + "SENDING_FIRST_STATUS_REQUEST", + "WAITING_FIRST_STATUS_ANSWER", + "SENDING_ALARM_STATUS_REQUEST", + "WAITING_ALARM_STATUS_ANSWER", + "IDLE", + "SENDING_STATUS_REQUEST", + "WAITING_STATUS_ANSWER", + "SENDING_UPDATE_SIGNAL_REQUEST", + "WAITING_UPDATE_SIGNAL_ANSWER", + "SENDING_SIGNAL_LEVEL", + "WAITING_SIGNAL_LEVEL_ANSWER", + "SENDING_CONTROL", + "WAITING_CONTROL_ANSWER", + "SENDING_POWER_ON_COMMAND", + "WAITING_POWER_ON_ANSWER", + "SENDING_POWER_OFF_COMMAND", + "WAITING_POWER_OFF_ANSWER", + "UNKNOWN" // Should be the last! + }; + int phase_index = (int) phase; + if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0)) + phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES; + return phase_names[phase_index]; +} +#endif + +HaierClimateBase::HaierClimateBase() + : haier_protocol_(*this), + protocol_phase_(ProtocolPhases::SENDING_INIT_1), + action_request_(ActionRequest::NO_ACTION), + display_status_(true), + health_mode_(false), + force_send_control_(false), + forced_publish_(false), + forced_request_status_(false), + first_control_attempt_(false), + reset_protocol_request_(false) { + this->traits_ = climate::ClimateTraits(); + this->traits_.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT, + climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY, + climate::CLIMATE_MODE_AUTO}); + this->traits_.set_supported_fan_modes( + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}); + this->traits_.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, + climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL}); + this->traits_.set_supports_current_temperature(true); +} + +HaierClimateBase::~HaierClimateBase() {} + +void HaierClimateBase::set_phase_(ProtocolPhases phase) { + if (this->protocol_phase_ != phase) { +#if (HAIER_LOG_LEVEL > 4) + ESP_LOGV(TAG, "Phase transition: %s => %s", phase_to_string_(this->protocol_phase_), phase_to_string_(phase)); +#else + ESP_LOGV(TAG, "Phase transition: %d => %d", (int) this->protocol_phase_, (int) phase); +#endif + this->protocol_phase_ = phase; + } +} + +bool HaierClimateBase::check_timeout_(std::chrono::steady_clock::time_point now, + std::chrono::steady_clock::time_point tpoint, size_t timeout) { + return std::chrono::duration_cast(now - tpoint).count() > timeout; +} + +bool HaierClimateBase::is_message_interval_exceeded_(std::chrono::steady_clock::time_point now) { + return this->check_timeout_(now, this->last_request_timestamp_, DEFAULT_MESSAGES_INTERVAL_MS); +} + +bool HaierClimateBase::is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now) { + return this->check_timeout_(now, this->last_status_request_, STATUS_REQUEST_INTERVAL_MS); +} + +bool HaierClimateBase::is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now) { + return this->check_timeout_(now, this->control_request_timestamp_, CONTROL_TIMEOUT_MS); +} + +bool HaierClimateBase::is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now) { + return this->check_timeout_(now, this->last_request_timestamp_, CONTROL_MESSAGES_INTERVAL_MS); +} + +bool HaierClimateBase::is_protocol_initialisation_interval_exceded_(std::chrono::steady_clock::time_point now) { + return this->check_timeout_(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL); +} + +bool HaierClimateBase::get_display_state() const { return this->display_status_; } + +void HaierClimateBase::set_display_state(bool state) { + if (this->display_status_ != state) { + this->display_status_ = state; + this->set_force_send_control_(true); + } +} + +bool HaierClimateBase::get_health_mode() const { return this->health_mode_; } + +void HaierClimateBase::set_health_mode(bool state) { + if (this->health_mode_ != state) { + this->health_mode_ = state; + this->set_force_send_control_(true); + } +} + +void HaierClimateBase::send_power_on_command() { this->action_request_ = ActionRequest::TURN_POWER_ON; } + +void HaierClimateBase::send_power_off_command() { this->action_request_ = ActionRequest::TURN_POWER_OFF; } + +void HaierClimateBase::toggle_power() { this->action_request_ = ActionRequest::TOGGLE_POWER; } +void HaierClimateBase::set_supported_swing_modes(const std::set &modes) { + this->traits_.set_supported_swing_modes(modes); + this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); // Always available + this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); // Always available +} + +void HaierClimateBase::set_supported_modes(const std::set &modes) { + this->traits_.set_supported_modes(modes); + this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available + this->traits_.add_supported_mode(climate::CLIMATE_MODE_AUTO); // Always available +} + +haier_protocol::HandlerError HaierClimateBase::answer_preprocess_(uint8_t request_message_type, + uint8_t expected_request_message_type, + uint8_t answer_message_type, + uint8_t expected_answer_message_type, + ProtocolPhases expected_phase) { + haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK; + if ((expected_request_message_type != NO_COMMAND) && (request_message_type != expected_request_message_type)) + result = haier_protocol::HandlerError::UNSUPORTED_MESSAGE; + if ((expected_answer_message_type != NO_COMMAND) && (answer_message_type != expected_answer_message_type)) + result = haier_protocol::HandlerError::UNSUPORTED_MESSAGE; + if ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_)) + result = haier_protocol::HandlerError::UNEXPECTED_MESSAGE; + if (is_message_invalid(answer_message_type)) + result = haier_protocol::HandlerError::INVALID_ANSWER; + return result; +} + +haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(uint8_t request_type) { +#if (HAIER_LOG_LEVEL > 4) + ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", request_type, phase_to_string_(this->protocol_phase_)); +#else + ESP_LOGW(TAG, "Answer timeout for command %02X, phase %d", request_type, (int) this->protocol_phase_); +#endif + if (this->protocol_phase_ > ProtocolPhases::IDLE) { + this->set_phase_(ProtocolPhases::IDLE); + } else { + this->set_phase_(ProtocolPhases::SENDING_INIT_1); + } + return haier_protocol::HandlerError::HANDLER_OK; +} + +void HaierClimateBase::setup() { + ESP_LOGI(TAG, "Haier initialization..."); + // Set timestamp here to give AC time to boot + this->last_request_timestamp_ = std::chrono::steady_clock::now(); + this->set_phase_(ProtocolPhases::SENDING_INIT_1); + this->set_answers_handlers(); + this->haier_protocol_.set_default_timeout_handler( + std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1)); +} + +void HaierClimateBase::dump_config() { + LOG_CLIMATE("", "Haier Climate", this); + ESP_LOGCONFIG(TAG, " Device communication status: %s", + (this->protocol_phase_ >= ProtocolPhases::IDLE) ? "established" : "none"); +} + +void HaierClimateBase::loop() { + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + if ((std::chrono::duration_cast(now - this->last_valid_status_timestamp_).count() > + COMMUNICATION_TIMEOUT_MS) || + (this->reset_protocol_request_)) { + if (this->protocol_phase_ >= ProtocolPhases::IDLE) { + // No status too long, reseting protocol + if (this->reset_protocol_request_) { + this->reset_protocol_request_ = false; + ESP_LOGW(TAG, "Protocol reset requested"); + } else { + ESP_LOGW(TAG, "Communication timeout, reseting protocol"); + } + this->last_valid_status_timestamp_ = now; + this->set_force_send_control_(false); + if (this->hvac_settings_.valid) + this->hvac_settings_.reset(); + this->set_phase_(ProtocolPhases::SENDING_INIT_1); + return; + } else { + // No need to reset protocol if we didn't pass initialization phase + this->last_valid_status_timestamp_ = now; + } + }; + if ((this->protocol_phase_ == ProtocolPhases::IDLE) || + (this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) || + (this->protocol_phase_ == ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST) || + (this->protocol_phase_ == ProtocolPhases::SENDING_SIGNAL_LEVEL)) { + // If control message or action is pending we should send it ASAP unless we are in initialisation + // procedure or waiting for an answer + if (this->action_request_ != ActionRequest::NO_ACTION) { + this->process_pending_action(); + } else if (this->hvac_settings_.valid || this->force_send_control_) { + ESP_LOGV(TAG, "Control packet is pending..."); + this->set_phase_(ProtocolPhases::SENDING_CONTROL); + } + } + this->process_phase(now); + this->haier_protocol_.loop(); +} + +void HaierClimateBase::process_pending_action() { + ActionRequest request = this->action_request_; + if (this->action_request_ == ActionRequest::TOGGLE_POWER) { + request = this->mode == CLIMATE_MODE_OFF ? ActionRequest::TURN_POWER_ON : ActionRequest::TURN_POWER_OFF; + } + switch (request) { + case ActionRequest::TURN_POWER_ON: + this->set_phase_(ProtocolPhases::SENDING_POWER_ON_COMMAND); + break; + case ActionRequest::TURN_POWER_OFF: + this->set_phase_(ProtocolPhases::SENDING_POWER_OFF_COMMAND); + break; + case ActionRequest::TOGGLE_POWER: + case ActionRequest::NO_ACTION: + // shouldn't get here, do nothing + break; + default: + ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_); + break; + } + this->action_request_ = ActionRequest::NO_ACTION; +} + +ClimateTraits HaierClimateBase::traits() { return traits_; } + +void HaierClimateBase::control(const ClimateCall &call) { + ESP_LOGD("Control", "Control call"); + if (this->protocol_phase_ < ProtocolPhases::IDLE) { + ESP_LOGW(TAG, "Can't send control packet, first poll answer not received"); + return; // cancel the control, we cant do it without a poll answer. + } + if (this->hvac_settings_.valid) { + ESP_LOGW(TAG, "Overriding old valid settings before they were applied!"); + } + { + if (call.get_mode().has_value()) + this->hvac_settings_.mode = call.get_mode(); + if (call.get_fan_mode().has_value()) + this->hvac_settings_.fan_mode = call.get_fan_mode(); + if (call.get_swing_mode().has_value()) + this->hvac_settings_.swing_mode = call.get_swing_mode(); + if (call.get_target_temperature().has_value()) + this->hvac_settings_.target_temperature = call.get_target_temperature(); + if (call.get_preset().has_value()) + this->hvac_settings_.preset = call.get_preset(); + this->hvac_settings_.valid = true; + } + this->first_control_attempt_ = true; +} + +void HaierClimateBase::HvacSettings::reset() { + this->valid = false; + this->mode.reset(); + this->fan_mode.reset(); + this->swing_mode.reset(); + this->target_temperature.reset(); + this->preset.reset(); +} + +void HaierClimateBase::set_force_send_control_(bool status) { + this->force_send_control_ = status; + if (status) { + this->first_control_attempt_ = true; + } +} + +void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc) { + this->haier_protocol_.send_message(command, use_crc); + this->last_request_timestamp_ = std::chrono::steady_clock::now(); +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h new file mode 100644 index 0000000000..046b59af96 --- /dev/null +++ b/esphome/components/haier/haier_base.h @@ -0,0 +1,142 @@ +#pragma once + +#include +#include +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" +// HaierProtocol +#include + +namespace esphome { +namespace haier { + +enum class ActionRequest : uint8_t { + NO_ACTION = 0, + TURN_POWER_ON = 1, + TURN_POWER_OFF = 2, + TOGGLE_POWER = 3, + START_SELF_CLEAN = 4, // only hOn + START_STERI_CLEAN = 5, // only hOn +}; + +class HaierClimateBase : public esphome::Component, + public esphome::climate::Climate, + public esphome::uart::UARTDevice, + public haier_protocol::ProtocolStream { + public: + HaierClimateBase(); + HaierClimateBase(const HaierClimateBase &) = delete; + HaierClimateBase &operator=(const HaierClimateBase &) = delete; + ~HaierClimateBase(); + void setup() override; + void loop() override; + void control(const esphome::climate::ClimateCall &call) override; + void dump_config() override; + float get_setup_priority() const override { return esphome::setup_priority::HARDWARE; } + void set_fahrenheit(bool fahrenheit); + void set_display_state(bool state); + bool get_display_state() const; + void set_health_mode(bool state); + bool get_health_mode() const; + void send_power_on_command(); + void send_power_off_command(); + void toggle_power(); + void reset_protocol() { this->reset_protocol_request_ = true; }; + void set_supported_modes(const std::set &modes); + void set_supported_swing_modes(const std::set &modes); + size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; + size_t read_array(uint8_t *data, size_t len) noexcept override { + return esphome::uart::UARTDevice::read_array(data, len) ? len : 0; + }; + void write_array(const uint8_t *data, size_t len) noexcept override { + esphome::uart::UARTDevice::write_array(data, len); + }; + bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; }; + + protected: + enum class ProtocolPhases { + UNKNOWN = -1, + // INITIALIZATION + SENDING_INIT_1 = 0, + WAITING_ANSWER_INIT_1 = 1, + SENDING_INIT_2 = 2, + WAITING_ANSWER_INIT_2 = 3, + SENDING_FIRST_STATUS_REQUEST = 4, + WAITING_FIRST_STATUS_ANSWER = 5, + SENDING_ALARM_STATUS_REQUEST = 6, + WAITING_ALARM_STATUS_ANSWER = 7, + // FUNCTIONAL STATE + IDLE = 8, + SENDING_STATUS_REQUEST = 9, + WAITING_STATUS_ANSWER = 10, + SENDING_UPDATE_SIGNAL_REQUEST = 11, + WAITING_UPDATE_SIGNAL_ANSWER = 12, + SENDING_SIGNAL_LEVEL = 13, + WAITING_SIGNAL_LEVEL_ANSWER = 14, + SENDING_CONTROL = 15, + WAITING_CONTROL_ANSWER = 16, + SENDING_POWER_ON_COMMAND = 17, + WAITING_POWER_ON_ANSWER = 18, + SENDING_POWER_OFF_COMMAND = 19, + WAITING_POWER_OFF_ANSWER = 20, + NUM_PROTOCOL_PHASES + }; +#if (HAIER_LOG_LEVEL > 4) + const char *phase_to_string_(ProtocolPhases phase); +#endif + virtual void set_answers_handlers() = 0; + virtual void process_phase(std::chrono::steady_clock::time_point now) = 0; + virtual haier_protocol::HaierMessage get_control_message() = 0; + virtual bool is_message_invalid(uint8_t message_type) = 0; + virtual void process_pending_action(); + esphome::climate::ClimateTraits traits() override; + // Answers handlers + haier_protocol::HandlerError answer_preprocess_(uint8_t request_message_type, uint8_t expected_request_message_type, + uint8_t answer_message_type, uint8_t expected_answer_message_type, + ProtocolPhases expected_phase); + // Timeout handler + haier_protocol::HandlerError timeout_default_handler_(uint8_t request_type); + // Helper functions + void set_force_send_control_(bool status); + void send_message_(const haier_protocol::HaierMessage &command, bool use_crc); + void set_phase_(ProtocolPhases phase); + bool check_timeout_(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint, + size_t timeout); + bool is_message_interval_exceeded_(std::chrono::steady_clock::time_point now); + bool is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now); + bool is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now); + bool is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now); + bool is_protocol_initialisation_interval_exceded_(std::chrono::steady_clock::time_point now); + + struct HvacSettings { + esphome::optional mode; + esphome::optional fan_mode; + esphome::optional swing_mode; + esphome::optional target_temperature; + esphome::optional preset; + bool valid; + HvacSettings() : valid(false){}; + void reset(); + }; + haier_protocol::ProtocolHandler haier_protocol_; + ProtocolPhases protocol_phase_; + ActionRequest action_request_; + uint8_t fan_mode_speed_; + uint8_t other_modes_fan_speed_; + bool display_status_; + bool health_mode_; + bool force_send_control_; + bool forced_publish_; + bool forced_request_status_; + bool first_control_attempt_; + bool reset_protocol_request_; + esphome::climate::ClimateTraits traits_; + HvacSettings hvac_settings_; + std::chrono::steady_clock::time_point last_request_timestamp_; // For interval between messages + std::chrono::steady_clock::time_point last_valid_status_timestamp_; // For protocol timeout + std::chrono::steady_clock::time_point last_status_request_; // To request AC status + std::chrono::steady_clock::time_point control_request_timestamp_; // To send control message +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp new file mode 100644 index 0000000000..3016cda397 --- /dev/null +++ b/esphome/components/haier/hon_climate.cpp @@ -0,0 +1,857 @@ +#include +#include +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" +#ifdef USE_WIFI +#include "esphome/components/wifi/wifi_component.h" +#endif +#include "hon_climate.h" +#include "hon_packet.h" + +using namespace esphome::climate; +using namespace esphome::uart; + +namespace esphome { +namespace haier { + +static const char *const TAG = "haier.climate"; +constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; +constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64; + +hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) { + switch (direction) { + case AirflowVerticalDirection::HEALTH_UP: + return hon_protocol::VerticalSwingMode::HEALTH_UP; + case AirflowVerticalDirection::MAX_UP: + return hon_protocol::VerticalSwingMode::MAX_UP; + case AirflowVerticalDirection::UP: + return hon_protocol::VerticalSwingMode::UP; + case AirflowVerticalDirection::DOWN: + return hon_protocol::VerticalSwingMode::DOWN; + case AirflowVerticalDirection::HEALTH_DOWN: + return hon_protocol::VerticalSwingMode::HEALTH_DOWN; + default: + return hon_protocol::VerticalSwingMode::CENTER; + } +} + +hon_protocol::HorizontalSwingMode get_horizontal_swing_mode(AirflowHorizontalDirection direction) { + switch (direction) { + case AirflowHorizontalDirection::MAX_LEFT: + return hon_protocol::HorizontalSwingMode::MAX_LEFT; + case AirflowHorizontalDirection::LEFT: + return hon_protocol::HorizontalSwingMode::LEFT; + case AirflowHorizontalDirection::RIGHT: + return hon_protocol::HorizontalSwingMode::RIGHT; + case AirflowHorizontalDirection::MAX_RIGHT: + return hon_protocol::HorizontalSwingMode::MAX_RIGHT; + default: + return hon_protocol::HorizontalSwingMode::CENTER; + } +} + +HonClimate::HonClimate() + : last_status_message_(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]), + cleaning_status_(CleaningState::NO_CLEANING), + got_valid_outdoor_temp_(false), + hvac_hardware_info_available_(false), + hvac_functions_{false, false, false, false, false}, + use_crc_(hvac_functions_[2]), + active_alarms_{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + outdoor_sensor_(nullptr), + send_wifi_signal_(true) { + this->traits_.set_supported_presets({ + climate::CLIMATE_PRESET_NONE, + climate::CLIMATE_PRESET_ECO, + climate::CLIMATE_PRESET_BOOST, + climate::CLIMATE_PRESET_SLEEP, + }); + this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID; + this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO; +} + +HonClimate::~HonClimate() {} + +void HonClimate::set_beeper_state(bool state) { this->beeper_status_ = state; } + +bool HonClimate::get_beeper_state() const { return this->beeper_status_; } + +void HonClimate::set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; } + +AirflowVerticalDirection HonClimate::get_vertical_airflow() const { return this->vertical_direction_; }; + +void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) { + if (direction > AirflowVerticalDirection::DOWN) { + this->vertical_direction_ = AirflowVerticalDirection::CENTER; + } else { + this->vertical_direction_ = direction; + } + this->set_force_send_control_(true); +} + +AirflowHorizontalDirection HonClimate::get_horizontal_airflow() const { return this->horizontal_direction_; } + +void HonClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) { + if (direction > AirflowHorizontalDirection::RIGHT) { + this->horizontal_direction_ = AirflowHorizontalDirection::CENTER; + } else { + this->horizontal_direction_ = direction; + } + this->set_force_send_control_(true); +} + +std::string HonClimate::get_cleaning_status_text() const { + switch (this->cleaning_status_) { + case CleaningState::SELF_CLEAN: + return "Self clean"; + case CleaningState::STERI_CLEAN: + return "56°C Steri-Clean"; + default: + return "No cleaning"; + } +} + +CleaningState HonClimate::get_cleaning_status() const { return this->cleaning_status_; } + +void HonClimate::start_self_cleaning() { + if (this->cleaning_status_ == CleaningState::NO_CLEANING) { + ESP_LOGI(TAG, "Sending self cleaning start request"); + this->action_request_ = ActionRequest::START_SELF_CLEAN; + this->set_force_send_control_(true); + } +} + +void HonClimate::start_steri_cleaning() { + if (this->cleaning_status_ == CleaningState::NO_CLEANING) { + ESP_LOGI(TAG, "Sending steri cleaning start request"); + this->action_request_ = ActionRequest::START_STERI_CLEAN; + this->set_force_send_control_(true); + } +} + +void HonClimate::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; } + +haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size) { + haier_protocol::HandlerError result = this->answer_preprocess_( + request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, message_type, + (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::WAITING_ANSWER_INIT_1); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + if (data_size < sizeof(hon_protocol::DeviceVersionAnswer)) { + // Wrong structure + this->set_phase_(ProtocolPhases::SENDING_INIT_1); + return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; + } + // All OK + hon_protocol::DeviceVersionAnswer *answr = (hon_protocol::DeviceVersionAnswer *) data; + char tmp[9]; + tmp[8] = 0; + strncpy(tmp, answr->protocol_version, 8); + this->hvac_protocol_version_ = std::string(tmp); + strncpy(tmp, answr->software_version, 8); + this->hvac_software_version_ = std::string(tmp); + strncpy(tmp, answr->hardware_version, 8); + this->hvac_hardware_version_ = std::string(tmp); + strncpy(tmp, answr->device_name, 8); + this->hvac_device_name_ = std::string(tmp); + this->hvac_functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support + this->hvac_functions_[1] = (answr->functions[1] & 0x02) != 0; // controller-device mode support + this->hvac_functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support + this->hvac_functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support + this->hvac_functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support + this->hvac_hardware_info_available_ = true; + this->set_phase_(ProtocolPhases::SENDING_INIT_2); + return result; + } else { + this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_INIT_1); + return result; + } +} + +haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size) { + haier_protocol::HandlerError result = this->answer_preprocess_( + request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID, message_type, + (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::WAITING_ANSWER_INIT_2); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + return result; + } else { + this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_INIT_1); + return result; + } +} + +haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size) { + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::CONTROL, message_type, + (uint8_t) hon_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + result = this->process_status_message_(data, data_size); + if (result != haier_protocol::HandlerError::HANDLER_OK) { + ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); + this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_INIT_1); + } else { + if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) { + memcpy(this->last_status_message_.get(), data + 2, sizeof(hon_protocol::HaierPacketControl)); + } else { + ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, + sizeof(hon_protocol::HaierPacketControl)); + } + if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) { + ESP_LOGI(TAG, "First HVAC status received"); + this->set_phase_(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST); + } else if ((this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) || + (this->protocol_phase_ == ProtocolPhases::WAITING_POWER_ON_ANSWER) || + (this->protocol_phase_ == ProtocolPhases::WAITING_POWER_OFF_ANSWER)) { + this->set_phase_(ProtocolPhases::IDLE); + } else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) { + this->set_phase_(ProtocolPhases::IDLE); + this->set_force_send_control_(false); + if (this->hvac_settings_.valid) + this->hvac_settings_.reset(); + } + } + return result; + } else { + this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_INIT_1); + return result; + } +} + +haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_(uint8_t request_type, + uint8_t message_type, + const uint8_t *data, + size_t data_size) { + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION, + message_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE, + ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + this->set_phase_(ProtocolPhases::SENDING_SIGNAL_LEVEL); + return result; + } else { + this->set_phase_(ProtocolPhases::IDLE); + return result; + } +} + +haier_protocol::HandlerError HonClimate::report_network_status_answer_handler_(uint8_t request_type, + uint8_t message_type, + const uint8_t *data, size_t data_size) { + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS, message_type, + (uint8_t) hon_protocol::FrameType::CONFIRM, ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); + this->set_phase_(ProtocolPhases::IDLE); + return result; +} + +haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size) { + if (request_type == (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS) { + if (message_type != (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) { + // Unexpected answer to request + this->set_phase_(ProtocolPhases::IDLE); + return haier_protocol::HandlerError::UNSUPORTED_MESSAGE; + } + if (this->protocol_phase_ != ProtocolPhases::WAITING_ALARM_STATUS_ANSWER) { + // Don't expect this answer now + this->set_phase_(ProtocolPhases::IDLE); + return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; + } + memcpy(this->active_alarms_, data + 2, 8); + this->set_phase_(ProtocolPhases::IDLE); + return haier_protocol::HandlerError::HANDLER_OK; + } else { + this->set_phase_(ProtocolPhases::IDLE); + return haier_protocol::HandlerError::UNSUPORTED_MESSAGE; + } +} + +void HonClimate::set_answers_handlers() { + // Set handlers + this->haier_protocol_.set_answer_handler( + (uint8_t) (hon_protocol::FrameType::GET_DEVICE_VERSION), + std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + (uint8_t) (hon_protocol::FrameType::GET_DEVICE_ID), + std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + (uint8_t) (hon_protocol::FrameType::CONTROL), + std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, + std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + (uint8_t) (hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION), + std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + (uint8_t) (hon_protocol::FrameType::GET_ALARM_STATUS), + std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + (uint8_t) (hon_protocol::FrameType::REPORT_NETWORK_STATUS), + std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); +} + +void HonClimate::dump_config() { + HaierClimateBase::dump_config(); + ESP_LOGCONFIG(TAG, " Protocol version: hOn"); + if (this->hvac_hardware_info_available_) { + ESP_LOGCONFIG(TAG, " Device protocol version: %s", this->hvac_protocol_version_.c_str()); + ESP_LOGCONFIG(TAG, " Device software version: %s", this->hvac_software_version_.c_str()); + ESP_LOGCONFIG(TAG, " Device hardware version: %s", this->hvac_hardware_version_.c_str()); + ESP_LOGCONFIG(TAG, " Device name: %s", this->hvac_device_name_.c_str()); + ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s", (this->hvac_functions_[0] ? " interactive" : ""), + (this->hvac_functions_[1] ? " controller-device" : ""), (this->hvac_functions_[2] ? " crc" : ""), + (this->hvac_functions_[3] ? " multinode" : ""), (this->hvac_functions_[4] ? " role" : "")); + ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str()); + } +} + +void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { + switch (this->protocol_phase_) { + case ProtocolPhases::SENDING_INIT_1: + if (this->can_send_message() && this->is_protocol_initialisation_interval_exceded_(now)) { + this->hvac_hardware_info_available_ = false; + // Indicate device capabilities: + // bit 0 - if 1 module support interactive mode + // bit 1 - if 1 module support controller-device mode + // bit 2 - if 1 module support crc + // bit 3 - if 1 module support multiple devices + // bit 4..bit 15 - not used + uint8_t module_capabilities[2] = {0b00000000, 0b00000111}; + static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( + (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities)); + this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_); + this->set_phase_(ProtocolPhases::WAITING_ANSWER_INIT_1); + } + break; + case ProtocolPhases::SENDING_INIT_2: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage DEVICEID_REQUEST((uint8_t) hon_protocol::FrameType::GET_DEVICE_ID); + this->send_message_(DEVICEID_REQUEST, this->use_crc_); + this->set_phase_(ProtocolPhases::WAITING_ANSWER_INIT_2); + } + break; + case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: + case ProtocolPhases::SENDING_STATUS_REQUEST: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage STATUS_REQUEST( + (uint8_t) hon_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcomandsControl::GET_USER_DATA); + this->send_message_(STATUS_REQUEST, this->use_crc_); + this->last_status_request_ = now; + this->set_phase_((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1)); + } + break; +#ifdef USE_WIFI + case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage UPDATE_SIGNAL_REQUEST( + (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION); + this->send_message_(UPDATE_SIGNAL_REQUEST, this->use_crc_); + this->last_signal_request_ = now; + this->set_phase_(ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER); + } + break; + case ProtocolPhases::SENDING_SIGNAL_LEVEL: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00}; + if (wifi::global_wifi_component->is_connected()) { + wifi_status_data[1] = 0; + int8_t rssi = wifi::global_wifi_component->wifi_rssi(); + wifi_status_data[3] = uint8_t((128 + rssi) / 1.28f); + ESP_LOGD(TAG, "WiFi signal is: %ddBm => %d%%", rssi, wifi_status_data[3]); + } else { + ESP_LOGD(TAG, "WiFi is not connected"); + wifi_status_data[1] = 1; + wifi_status_data[3] = 0; + } + haier_protocol::HaierMessage wifi_status_request((uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS, + wifi_status_data, sizeof(wifi_status_data)); + this->send_message_(wifi_status_request, this->use_crc_); + this->set_phase_(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); + } + break; + case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: + case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: + break; +#else + case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: + case ProtocolPhases::SENDING_SIGNAL_LEVEL: + case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: + case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: + this->set_phase_(ProtocolPhases::IDLE); + break; +#endif + case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST( + (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS); + this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_); + this->set_phase_(ProtocolPhases::WAITING_ALARM_STATUS_ANSWER); + } + break; + case ProtocolPhases::SENDING_CONTROL: + if (this->first_control_attempt_) { + this->control_request_timestamp_ = now; + this->first_control_attempt_ = false; + } + if (this->is_control_message_timeout_exceeded_(now)) { + ESP_LOGW(TAG, "Sending control packet timeout!"); + this->set_force_send_control_(false); + if (this->hvac_settings_.valid) + this->hvac_settings_.reset(); + this->forced_request_status_ = true; + this->forced_publish_ = true; + this->set_phase_(ProtocolPhases::IDLE); + } else if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) { + haier_protocol::HaierMessage control_message = get_control_message(); + this->send_message_(control_message, this->use_crc_); + ESP_LOGI(TAG, "Control packet sent"); + this->set_phase_(ProtocolPhases::WAITING_CONTROL_ANSWER); + } + break; + case ProtocolPhases::SENDING_POWER_ON_COMMAND: + case ProtocolPhases::SENDING_POWER_OFF_COMMAND: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + uint8_t pwr_cmd_buf[2] = {0x00, 0x00}; + if (this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND) + pwr_cmd_buf[1] = 0x01; + haier_protocol::HaierMessage power_cmd((uint8_t) hon_protocol::FrameType::CONTROL, + ((uint16_t) hon_protocol::SubcomandsControl::SET_SINGLE_PARAMETER) + 1, + pwr_cmd_buf, sizeof(pwr_cmd_buf)); + this->send_message_(power_cmd, this->use_crc_); + this->set_phase_(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND + ? ProtocolPhases::WAITING_POWER_ON_ANSWER + : ProtocolPhases::WAITING_POWER_OFF_ANSWER); + } + break; + + case ProtocolPhases::WAITING_ANSWER_INIT_1: + case ProtocolPhases::WAITING_ANSWER_INIT_2: + case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER: + case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER: + case ProtocolPhases::WAITING_STATUS_ANSWER: + case ProtocolPhases::WAITING_CONTROL_ANSWER: + case ProtocolPhases::WAITING_POWER_ON_ANSWER: + case ProtocolPhases::WAITING_POWER_OFF_ANSWER: + break; + case ProtocolPhases::IDLE: { + if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { + this->set_phase_(ProtocolPhases::SENDING_STATUS_REQUEST); + this->forced_request_status_ = false; + } +#ifdef USE_WIFI + else if (this->send_wifi_signal_ && + (std::chrono::duration_cast(now - this->last_signal_request_).count() > + SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) + this->set_phase_(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST); +#endif + } break; + default: + // Shouldn't get here +#if (HAIER_LOG_LEVEL > 4) + ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication", + phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_); +#else + ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_); +#endif + this->set_phase_(ProtocolPhases::SENDING_INIT_1); + break; + } +} + +haier_protocol::HaierMessage HonClimate::get_control_message() { + uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)]; + memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl)); + hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer; + bool has_hvac_settings = false; + if (this->hvac_settings_.valid) { + has_hvac_settings = true; + HvacSettings climate_control; + climate_control = this->hvac_settings_; + if (climate_control.mode.has_value()) { + switch (climate_control.mode.value()) { + case CLIMATE_MODE_OFF: + out_data->ac_power = 0; + break; + case CLIMATE_MODE_AUTO: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::AUTO; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_HEAT: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::HEAT; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_DRY: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_FAN_ONLY: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::FAN; + out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode + // Disabling boost and eco mode for Fan only + out_data->quiet_mode = 0; + out_data->fast_mode = 0; + break; + case CLIMATE_MODE_COOL: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::COOL; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + default: + ESP_LOGE("Control", "Unsupported climate mode"); + break; + } + } + // Set fan speed, if we are in fan mode, reject auto in fan mode + if (climate_control.fan_mode.has_value()) { + switch (climate_control.fan_mode.value()) { + case CLIMATE_FAN_LOW: + out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_LOW; + break; + case CLIMATE_FAN_MEDIUM: + out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_MID; + break; + case CLIMATE_FAN_HIGH: + out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_HIGH; + break; + case CLIMATE_FAN_AUTO: + if (mode != CLIMATE_MODE_FAN_ONLY) // if we are not in fan only mode + out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_AUTO; + break; + default: + ESP_LOGE("Control", "Unsupported fan mode"); + break; + } + } + // Set swing mode + if (climate_control.swing_mode.has_value()) { + switch (climate_control.swing_mode.value()) { + case CLIMATE_SWING_OFF: + out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_); + out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_); + break; + case CLIMATE_SWING_VERTICAL: + out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_); + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::AUTO; + break; + case CLIMATE_SWING_HORIZONTAL: + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO; + out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_); + break; + case CLIMATE_SWING_BOTH: + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO; + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::AUTO; + break; + } + } + if (climate_control.target_temperature.has_value()) { + out_data->set_point = + climate_control.target_temperature.value() - 16; // set the temperature at our offset, subtract 16. + } + if (out_data->ac_power == 0) { + // If AC is off - no presets alowed + out_data->quiet_mode = 0; + out_data->fast_mode = 0; + out_data->sleep_mode = 0; + } else if (climate_control.preset.has_value()) { + switch (climate_control.preset.value()) { + case CLIMATE_PRESET_NONE: + out_data->quiet_mode = 0; + out_data->fast_mode = 0; + out_data->sleep_mode = 0; + break; + case CLIMATE_PRESET_ECO: + // Eco is not supported in Fan only mode + out_data->quiet_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0; + out_data->fast_mode = 0; + out_data->sleep_mode = 0; + break; + case CLIMATE_PRESET_BOOST: + out_data->quiet_mode = 0; + // Boost is not supported in Fan only mode + out_data->fast_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0; + out_data->sleep_mode = 0; + break; + case CLIMATE_PRESET_AWAY: + out_data->quiet_mode = 0; + out_data->fast_mode = 0; + out_data->sleep_mode = 0; + break; + case CLIMATE_PRESET_SLEEP: + out_data->quiet_mode = 0; + out_data->fast_mode = 0; + out_data->sleep_mode = 1; + break; + default: + ESP_LOGE("Control", "Unsupported preset"); + break; + } + } + } else { + if (out_data->vertical_swing_mode != (uint8_t) hon_protocol::VerticalSwingMode::AUTO) + out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_); + if (out_data->horizontal_swing_mode != (uint8_t) hon_protocol::HorizontalSwingMode::AUTO) + out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_); + } + out_data->beeper_status = ((!this->beeper_status_) || (!has_hvac_settings)) ? 1 : 0; + control_out_buffer[4] = 0; // This byte should be cleared before setting values + out_data->display_status = this->display_status_ ? 1 : 0; + out_data->health_mode = this->health_mode_ ? 1 : 0; + switch (this->action_request_) { + case ActionRequest::START_SELF_CLEAN: + this->action_request_ = ActionRequest::NO_ACTION; + out_data->self_cleaning_status = 1; + out_data->steri_clean = 0; + out_data->set_point = 0x06; + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER; + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER; + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; + out_data->light_status = 0; + break; + case ActionRequest::START_STERI_CLEAN: + this->action_request_ = ActionRequest::NO_ACTION; + out_data->self_cleaning_status = 0; + out_data->steri_clean = 1; + out_data->set_point = 0x06; + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER; + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER; + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; + out_data->light_status = 0; + break; + default: + // No change + break; + } + return haier_protocol::HaierMessage((uint8_t) hon_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcomandsControl::SET_GROUP_PARAMETERS, + control_out_buffer, sizeof(hon_protocol::HaierPacketControl)); +} + +haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { + if (size < sizeof(hon_protocol::HaierStatus)) + return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; + hon_protocol::HaierStatus packet; + if (size < sizeof(hon_protocol::HaierStatus)) + size = sizeof(hon_protocol::HaierStatus); + memcpy(&packet, packet_buffer, size); + if (packet.sensors.error_status != 0) { + ESP_LOGW(TAG, "HVAC error, code=0x%02X", packet.sensors.error_status); + } + if ((this->outdoor_sensor_ != nullptr) && (got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) { + got_valid_outdoor_temp_ = true; + float otemp = (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET); + if ((!this->outdoor_sensor_->has_state()) || (this->outdoor_sensor_->get_raw_state() != otemp)) + this->outdoor_sensor_->publish_state(otemp); + } + bool should_publish = false; + { + // Extra modes/presets + optional old_preset = this->preset; + if (packet.control.quiet_mode != 0) { + this->preset = CLIMATE_PRESET_ECO; + } else if (packet.control.fast_mode != 0) { + this->preset = CLIMATE_PRESET_BOOST; + } else if (packet.control.sleep_mode != 0) { + this->preset = CLIMATE_PRESET_SLEEP; + } else { + this->preset = CLIMATE_PRESET_NONE; + } + should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value()); + } + { + // Target temperature + float old_target_temperature = this->target_temperature; + this->target_temperature = packet.control.set_point + 16.0f; + should_publish = should_publish || (old_target_temperature != this->target_temperature); + } + { + // Current temperature + float old_current_temperature = this->current_temperature; + this->current_temperature = packet.sensors.room_temperature / 2.0f; + should_publish = should_publish || (old_current_temperature != this->current_temperature); + } + { + // Fan mode + optional old_fan_mode = this->fan_mode; + // remember the fan speed we last had for climate vs fan + if (packet.control.ac_mode == (uint8_t) hon_protocol::ConditioningMode::FAN) { + if (packet.control.fan_mode != (uint8_t) hon_protocol::FanMode::FAN_AUTO) + this->fan_mode_speed_ = packet.control.fan_mode; + } else { + this->other_modes_fan_speed_ = packet.control.fan_mode; + } + switch (packet.control.fan_mode) { + case (uint8_t) hon_protocol::FanMode::FAN_AUTO: + if (packet.control.ac_mode != (uint8_t) hon_protocol::ConditioningMode::FAN) { + this->fan_mode = CLIMATE_FAN_AUTO; + } else { + // Shouldn't accept fan speed auto in fan-only mode even if AC reports it + ESP_LOGI(TAG, "Fan speed Auto is not supported in Fan only AC mode, ignoring"); + } + break; + case (uint8_t) hon_protocol::FanMode::FAN_MID: + this->fan_mode = CLIMATE_FAN_MEDIUM; + break; + case (uint8_t) hon_protocol::FanMode::FAN_LOW: + this->fan_mode = CLIMATE_FAN_LOW; + break; + case (uint8_t) hon_protocol::FanMode::FAN_HIGH: + this->fan_mode = CLIMATE_FAN_HIGH; + break; + } + should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value()); + } + { + // Display status + // should be before "Climate mode" because it is changing this->mode + if (packet.control.ac_power != 0) { + // if AC is off display status always ON so process it only when AC is on + bool disp_status = packet.control.display_status != 0; + if (disp_status != this->display_status_) { + // Do something only if display status changed + if (this->mode == CLIMATE_MODE_OFF) { + // AC just turned on from remote need to turn off display + this->set_force_send_control_(true); + } else { + this->display_status_ = disp_status; + } + } + } + } + { + // Health mode + bool old_health_mode = this->health_mode_; + this->health_mode_ = packet.control.health_mode == 1; + should_publish = should_publish || (old_health_mode != this->health_mode_); + } + { + CleaningState new_cleaning; + if (packet.control.steri_clean == 1) { + // Steri-cleaning + new_cleaning = CleaningState::STERI_CLEAN; + } else if (packet.control.self_cleaning_status == 1) { + // Self-cleaning + new_cleaning = CleaningState::SELF_CLEAN; + } else { + // No cleaning + new_cleaning = CleaningState::NO_CLEANING; + } + if (new_cleaning != this->cleaning_status_) { + ESP_LOGD(TAG, "Cleaning status change: %d => %d", (uint8_t) this->cleaning_status_, (uint8_t) new_cleaning); + if (new_cleaning == CleaningState::NO_CLEANING) { + // Turnuin AC off after cleaning + this->action_request_ = ActionRequest::TURN_POWER_OFF; + } + this->cleaning_status_ = new_cleaning; + } + } + { + // Climate mode + ClimateMode old_mode = this->mode; + if (packet.control.ac_power == 0) { + this->mode = CLIMATE_MODE_OFF; + } else { + // Check current hvac mode + switch (packet.control.ac_mode) { + case (uint8_t) hon_protocol::ConditioningMode::COOL: + this->mode = CLIMATE_MODE_COOL; + break; + case (uint8_t) hon_protocol::ConditioningMode::HEAT: + this->mode = CLIMATE_MODE_HEAT; + break; + case (uint8_t) hon_protocol::ConditioningMode::DRY: + this->mode = CLIMATE_MODE_DRY; + break; + case (uint8_t) hon_protocol::ConditioningMode::FAN: + this->mode = CLIMATE_MODE_FAN_ONLY; + break; + case (uint8_t) hon_protocol::ConditioningMode::AUTO: + this->mode = CLIMATE_MODE_AUTO; + break; + } + } + should_publish = should_publish || (old_mode != this->mode); + } + { + // Swing mode + ClimateSwingMode old_swing_mode = this->swing_mode; + if (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO) { + if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) { + this->swing_mode = CLIMATE_SWING_BOTH; + } else { + this->swing_mode = CLIMATE_SWING_HORIZONTAL; + } + } else { + if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) { + this->swing_mode = CLIMATE_SWING_VERTICAL; + } else { + this->swing_mode = CLIMATE_SWING_OFF; + } + } + should_publish = should_publish || (old_swing_mode != this->swing_mode); + } + this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); + if (this->forced_publish_ || should_publish) { +#if (HAIER_LOG_LEVEL > 4) + std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now(); +#endif + this->publish_state(); +#if (HAIER_LOG_LEVEL > 4) + ESP_LOGV(TAG, "Publish delay: %lld ms", + std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - + _publish_start) + .count()); +#endif + this->forced_publish_ = false; + } + if (should_publish) { + ESP_LOGI(TAG, "HVAC values changed"); + } + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "HVAC Mode = 0x%X", packet.control.ac_mode); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Fan speed Status = 0x%X", packet.control.fan_mode); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing_mode); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Vertical Swing Status = 0x%X", packet.control.vertical_swing_mode); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Set Point Status = 0x%X", packet.control.set_point); + return haier_protocol::HandlerError::HANDLER_OK; +} + +bool HonClimate::is_message_invalid(uint8_t message_type) { + return message_type == (uint8_t) hon_protocol::FrameType::INVALID; +} + +void HonClimate::process_pending_action() { + switch (this->action_request_) { + case ActionRequest::START_SELF_CLEAN: + case ActionRequest::START_STERI_CLEAN: + // Will reset action with control message sending + this->set_phase_(ProtocolPhases::SENDING_CONTROL); + break; + default: + HaierClimateBase::process_pending_action(); + break; + } +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h new file mode 100644 index 0000000000..ab913f44e2 --- /dev/null +++ b/esphome/components/haier/hon_climate.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#include "esphome/components/sensor/sensor.h" +#include "haier_base.h" + +namespace esphome { +namespace haier { + +enum class AirflowVerticalDirection : uint8_t { + HEALTH_UP = 0, + MAX_UP = 1, + UP = 2, + CENTER = 3, + DOWN = 4, + HEALTH_DOWN = 5, +}; + +enum class AirflowHorizontalDirection : uint8_t { + MAX_LEFT = 0, + LEFT = 1, + CENTER = 2, + RIGHT = 3, + MAX_RIGHT = 4, +}; + +enum class CleaningState : uint8_t { + NO_CLEANING = 0, + SELF_CLEAN = 1, + STERI_CLEAN = 2, +}; + +class HonClimate : public HaierClimateBase { + public: + HonClimate(); + HonClimate(const HonClimate &) = delete; + HonClimate &operator=(const HonClimate &) = delete; + ~HonClimate(); + void dump_config() override; + void set_beeper_state(bool state); + bool get_beeper_state() const; + void set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor); + AirflowVerticalDirection get_vertical_airflow() const; + void set_vertical_airflow(AirflowVerticalDirection direction); + AirflowHorizontalDirection get_horizontal_airflow() const; + void set_horizontal_airflow(AirflowHorizontalDirection direction); + std::string get_cleaning_status_text() const; + CleaningState get_cleaning_status() const; + void start_self_cleaning(); + void start_steri_cleaning(); + void set_send_wifi(bool send_wifi); + + protected: + void set_answers_handlers() override; + void process_phase(std::chrono::steady_clock::time_point now) override; + haier_protocol::HaierMessage get_control_message() override; + bool is_message_invalid(uint8_t message_type) override; + void process_pending_action() override; + + // Answers handlers + haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data, + size_t data_size); + haier_protocol::HandlerError get_management_information_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + // Helper functions + haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); + std::unique_ptr last_status_message_; + bool beeper_status_; + CleaningState cleaning_status_; + bool got_valid_outdoor_temp_; + AirflowVerticalDirection vertical_direction_; + AirflowHorizontalDirection horizontal_direction_; + bool hvac_hardware_info_available_; + std::string hvac_protocol_version_; + std::string hvac_software_version_; + std::string hvac_hardware_version_; + std::string hvac_device_name_; + bool hvac_functions_[5]; + bool &use_crc_; + uint8_t active_alarms_[8]; + esphome::sensor::Sensor *outdoor_sensor_; + bool send_wifi_signal_; + std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/hon_packet.h b/esphome/components/haier/hon_packet.h new file mode 100644 index 0000000000..d572ce80d9 --- /dev/null +++ b/esphome/components/haier/hon_packet.h @@ -0,0 +1,228 @@ +#pragma once + +#include + +namespace esphome { +namespace haier { +namespace hon_protocol { + +enum class VerticalSwingMode : uint8_t { + HEALTH_UP = 0x01, + MAX_UP = 0x02, + HEALTH_DOWN = 0x03, + UP = 0x04, + CENTER = 0x06, + DOWN = 0x08, + AUTO = 0x0C +}; + +enum class HorizontalSwingMode : uint8_t { + CENTER = 0x00, + MAX_LEFT = 0x03, + LEFT = 0x04, + RIGHT = 0x05, + MAX_RIGHT = 0x06, + AUTO = 0x07 +}; + +enum class ConditioningMode : uint8_t { + AUTO = 0x00, + COOL = 0x01, + DRY = 0x02, + HEALTHY_DRY = 0x03, + HEAT = 0x04, + ENERGY_SAVING = 0x05, + FAN = 0x06 +}; + +enum class SpecialMode : uint8_t { NONE = 0x00, ELDERLY = 0x01, CHILDREN = 0x02, PREGNANT = 0x03 }; + +enum class FanMode : uint8_t { FAN_HIGH = 0x01, FAN_MID = 0x02, FAN_LOW = 0x03, FAN_AUTO = 0x05 }; + +struct HaierPacketControl { + // Control bytes starts here + // 10 + uint8_t set_point; // Target temperature with 16°C offset (0x00 = 16°C) + // 11 + uint8_t vertical_swing_mode : 4; // See enum VerticalSwingMode + uint8_t : 0; + // 12 + uint8_t fan_mode : 3; // See enum FanMode + uint8_t special_mode : 2; // See enum SpecialMode + uint8_t ac_mode : 3; // See enum ConditioningMode + // 13 + uint8_t : 8; + // 14 + uint8_t ten_degree : 1; // 10 degree status + uint8_t display_status : 1; // If 0 disables AC's display + uint8_t half_degree : 1; // Use half degree + uint8_t intelegence_status : 1; // Intelligence status + uint8_t pmv_status : 1; // Comfort/PMV status + uint8_t use_fahrenheit : 1; // Use Fahrenheit instead of Celsius + uint8_t : 1; + uint8_t steri_clean : 1; + // 15 + uint8_t ac_power : 1; // Is ac on or off + uint8_t health_mode : 1; // Health mode (negative ions) on or off + uint8_t electric_heating_status : 1; // Electric heating status + uint8_t fast_mode : 1; // Fast mode + uint8_t quiet_mode : 1; // Quiet mode + uint8_t sleep_mode : 1; // Sleep mode + uint8_t lock_remote : 1; // Disable remote + uint8_t beeper_status : 1; // If 1 disables AC's command feedback beeper (need to be set on every control command) + // 16 + uint8_t target_humidity; // Target humidity (0=30% .. 3C=90%, step = 1%) + // 17 + uint8_t horizontal_swing_mode : 3; // See enum HorizontalSwingMode + uint8_t : 3; + uint8_t human_sensing_status : 2; // Human sensing status + // 18 + uint8_t change_filter : 1; // Filter need replacement + uint8_t : 0; + // 19 + uint8_t fresh_air_status : 1; // Fresh air status + uint8_t humidification_status : 1; // Humidification status + uint8_t pm2p5_cleaning_status : 1; // PM2.5 cleaning status + uint8_t ch2o_cleaning_status : 1; // CH2O cleaning status + uint8_t self_cleaning_status : 1; // Self cleaning status + uint8_t light_status : 1; // Light status + uint8_t energy_saving_status : 1; // Energy saving status + uint8_t cleaning_time_status : 1; // Cleaning time (0 - accumulation, 1 - clear) +}; + +struct HaierPacketSensors { + // 20 + uint8_t room_temperature; // 0.5°C step + // 21 + uint8_t room_humidity; // 0%-100% with 1% step + // 22 + uint8_t outdoor_temperature; // 1°C step, -64°C offset (0=-64°C) + // 23 + uint8_t pm2p5_level : 2; // Indoor PM2.5 grade (00: Excellent, 01: good, 02: Medium, 03: Bad) + uint8_t air_quality : 2; // Air quality grade (00: Excellent, 01: good, 02: Medium, 03: Bad) + uint8_t human_sensing : 2; // Human presence result (00: N/A, 01: not detected, 02: One, 03: Multiple) + uint8_t : 1; + uint8_t ac_type : 1; // 00 - Heat and cool, 01 - Cool only) + // 24 + uint8_t error_status; // See enum ErrorStatus + // 25 + uint8_t operation_source : 2; // who is controlling AC (00: Other, 01: Remote control, 02: Button, 03: ESP) + uint8_t operation_mode_hk : 2; // Homekit only, operation mode (00: Cool, 01: Dry, 02: Heat, 03: Fan) + uint8_t : 3; + uint8_t err_confirmation : 1; // If 1 clear error status + // 26 + uint16_t total_cleaning_time; // Cleaning cumulative time (1h step) + // 28 + uint16_t indoor_pm2p5_value; // Indoor PM2.5 value (0 ug/m3 - 4095 ug/m3, 1 ug/m3 step) + // 30 + uint16_t outdoor_pm2p5_value; // Outdoor PM2.5 value (0 ug/m3 - 4095 ug/m3, 1 ug/m3 step) + // 32 + uint16_t ch2o_value; // Formaldehyde value (0 ug/m3 - 10000 ug/m3, 1 ug/m3 step) + // 34 + uint16_t voc_value; // VOC value (Volatile Organic Compounds) (0 ug/m3 - 1023 ug/m3, 1 ug/m3 step) + // 36 + uint16_t co2_value; // CO2 value (0 PPM - 10000 PPM, 1 PPM step) +}; + +struct HaierStatus { + uint16_t subcommand; + HaierPacketControl control; + HaierPacketSensors sensors; +}; + +struct DeviceVersionAnswer { + char protocol_version[8]; + char software_version[8]; + uint8_t encryption[3]; + char hardware_version[8]; + uint8_t : 8; + char device_name[8]; + uint8_t functions[2]; +}; + +// In this section comments: +// - module is the ESP32 control module (communication module in Haier protocol document) +// - device is the conditioner control board (network appliances in Haier protocol document) +enum class FrameType : uint8_t { + CONTROL = 0x01, // Requests or sets one or multiple parameters (module <-> device, required) + STATUS = 0x02, // Contains one or multiple parameters values, usually answer to control frame (module <-> device, + // required) + INVALID = 0x03, // Communication error indication (module <-> device, required) + ALARM_STATUS = 0x04, // Alarm status report (module <-> device, interactive, required) + CONFIRM = 0x05, // Acknowledgment, usually used to confirm reception of frame if there is no special answer (module + // <-> device, required) + REPORT = 0x06, // Report frame (module <-> device, interactive, required) + STOP_FAULT_ALARM = 0x09, // Stop fault alarm frame (module -> device, interactive, required) + SYSTEM_DOWNLIK = 0x11, // System downlink frame (module -> device, optional) + DEVICE_UPLINK = 0x12, // Device uplink frame (module <- device , interactive, optional) + SYSTEM_QUERY = 0x13, // System query frame (module -> device, optional) + SYSTEM_QUERY_RESPONSE = 0x14, // System query response frame (module <- device , optional) + DEVICE_QUERY = 0x15, // Device query frame (module <- device, optional) + DEVICE_QUERY_RESPONSE = 0x16, // Device query response frame (module -> device, optional) + GROUP_COMMAND = 0x60, // Group command frame (module -> device, interactive, optional) + GET_DEVICE_VERSION = 0x61, // Requests device version (module -> device, required) + GET_DEVICE_VERSION_RESPONSE = 0x62, // Device version answer (module <- device, required_ + GET_ALL_ADDRESSES = 0x67, // Requests all devices addresses (module -> device, interactive, optional) + GET_ALL_ADDRESSES_RESPONSE = + 0x68, // Answer to request of all devices addresses (module <- device , interactive, optional) + HANDSET_CHANGE_NOTIFICATION = 0x69, // Handset change notification frame (module <- device , interactive, optional) + GET_DEVICE_ID = 0x70, // Requests Device ID (module -> device, required) + GET_DEVICE_ID_RESPONSE = 0x71, // Response to device ID request (module <- device , required) + GET_ALARM_STATUS = 0x73, // Alarm status request (module -> device, required) + GET_ALARM_STATUS_RESPONSE = 0x74, // Response to alarm status request (module <- device, required) + GET_DEVICE_CONFIGURATION = 0x7C, // Requests device configuration (module -> device, interactive, required) + GET_DEVICE_CONFIGURATION_RESPONSE = + 0x7D, // Response to device configuration request (module <- device, interactive, required) + DOWNLINK_TRANSPARENT_TRANSMISSION = 0x8C, // Downlink transparent transmission (proxy data Haier cloud -> device) + // (module -> device, interactive, optional) + UPLINK_TRANSPARENT_TRANSMISSION = 0x8D, // Uplink transparent transmission (proxy data device -> Haier cloud) (module + // <- device, interactive, optional) + START_DEVICE_UPGRADE = 0xE1, // Initiate device OTA upgrade (module -> device, OTA required) + START_DEVICE_UPGRADE_RESPONSE = 0xE2, // Response to initiate device upgrade command (module <- device, OTA required) + GET_FIRMWARE_CONTENT = 0xE5, // Requests to send firmware (module <- device, OTA required) + GET_FIRMWARE_CONTENT_RESPONSE = + 0xE6, // Response to send firmware request (module -> device, OTA required) (multipacket?) + CHANGE_BAUD_RATE = 0xE7, // Requests to change port baud rate (module <- device, OTA required) + CHANGE_BAUD_RATE_RESPONSE = 0xE8, // Response to change port baud rate request (module -> device, OTA required) + GET_SUBBOARD_INFO = 0xE9, // Requests subboard information (module -> device, required) + GET_SUBBOARD_INFO_RESPONSE = 0xEA, // Response to subboard information request (module <- device, required) + GET_HARDWARE_INFO = 0xEB, // Requests information about device and subboard (module -> device, required) + GET_HARDWARE_INFO_RESPONSE = 0xEC, // Response to hardware information request (module <- device, required) + GET_UPGRADE_RESULT = 0xED, // Requests result of the firmware update (module <- device, OTA required) + GET_UPGRADE_RESULT_RESPONSE = 0xEF, // Response to firmware update results request (module -> device, OTA required) + GET_NETWORK_STATUS = 0xF0, // Requests network status (module <- device, interactive, optional) + GET_NETWORK_STATUS_RESPONSE = 0xF1, // Response to network status request (module -> device, interactive, optional) + START_WIFI_CONFIGURATION = 0xF2, // Starts WiFi configuration procedure (module <- device, interactive, required) + START_WIFI_CONFIGURATION_RESPONSE = + 0xF3, // Response to start WiFi configuration request (module -> device, interactive, required) + STOP_WIFI_CONFIGURATION = 0xF4, // Stop WiFi configuration procedure (module <- device, interactive, required) + STOP_WIFI_CONFIGURATION_RESPONSE = + 0xF5, // Response to stop WiFi configuration request (module -> device, interactive, required) + REPORT_NETWORK_STATUS = 0xF7, // Reports network status (module -> device, required) + CLEAR_CONFIGURATION = 0xF8, // Request to clear module configuration (module <- device, interactive, optional) + BIG_DATA_REPORT_CONFIGURATION = + 0xFA, // Configuration for autoreport device full status (module -> device, interactive, optional) + BIG_DATA_REPORT_CONFIGURATION_RESPONSE = + 0xFB, // Response to set big data configuration (module <- device, interactive, optional) + GET_MANAGEMENT_INFORMATION = 0xFC, // Request management information from device (module -> device, required) + GET_MANAGEMENT_INFORMATION_RESPONSE = + 0xFD, // Response to management information request (module <- device, required) + WAKE_UP = 0xFE, // Request to wake up (module <-> device, optional) +}; + +enum class SubcomandsControl : uint16_t { + GET_PARAMETERS = 0x4C01, // Request specific parameters (packet content: parameter ID1 + parameter ID2 + ...) + GET_USER_DATA = 0x4D01, // Request all user data from device (packet content: None) + GET_BIG_DATA = 0x4DFE, // Request big data information from device (packet content: None) + SET_PARAMETERS = 0x5C01, // Set parameters of the device and device return parameters (packet content: parameter ID1 + // + parameter data1 + parameter ID2 + parameter data 2 + ...) + SET_SINGLE_PARAMETER = 0x5D00, // Set single parameter (0x5DXX second byte define parameter ID) and return all user + // data (packet content: ???) + SET_GROUP_PARAMETERS = 0x6001, // Set group parameters to device (0x60XX second byte define parameter is group ID, + // the only group mentioned in document is 1) and return all user data (packet + // content: all values like in status packet) +}; + +} // namespace hon_protocol +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/logger_handler.cpp b/esphome/components/haier/logger_handler.cpp new file mode 100644 index 0000000000..f886318097 --- /dev/null +++ b/esphome/components/haier/logger_handler.cpp @@ -0,0 +1,33 @@ +#include "logger_handler.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace haier { + +void esphome_logger(haier_protocol::HaierLogLevel level, const char *tag, const char *message) { + switch (level) { + case haier_protocol::HaierLogLevel::LEVEL_ERROR: + esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, "%s", message); + break; + case haier_protocol::HaierLogLevel::LEVEL_WARNING: + esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, "%s", message); + break; + case haier_protocol::HaierLogLevel::LEVEL_INFO: + esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, "%s", message); + break; + case haier_protocol::HaierLogLevel::LEVEL_DEBUG: + esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, "%s", message); + break; + case haier_protocol::HaierLogLevel::LEVEL_VERBOSE: + esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, "%s", message); + break; + default: + // Just ignore everything else + break; + } +} + +void init_haier_protocol_logging() { haier_protocol::set_log_handler(esphome::haier::esphome_logger); }; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/logger_handler.h b/esphome/components/haier/logger_handler.h new file mode 100644 index 0000000000..2955468f37 --- /dev/null +++ b/esphome/components/haier/logger_handler.h @@ -0,0 +1,14 @@ +#pragma once + +// HaierProtocol +#include + +namespace esphome { +namespace haier { + +// This file is called in the code generated by python script +// Do not use it directly! +void init_haier_protocol_logging(); + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp new file mode 100644 index 0000000000..9c0fbac350 --- /dev/null +++ b/esphome/components/haier/smartair2_climate.cpp @@ -0,0 +1,457 @@ +#include +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" +#include "smartair2_climate.h" +#include "smartair2_packet.h" + +using namespace esphome::climate; +using namespace esphome::uart; + +namespace esphome { +namespace haier { + +static const char *const TAG = "haier.climate"; + +Smartair2Climate::Smartair2Climate() + : last_status_message_(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]) { + this->traits_.set_supported_presets({ + climate::CLIMATE_PRESET_NONE, + climate::CLIMATE_PRESET_BOOST, + climate::CLIMATE_PRESET_COMFORT, + }); +} + +haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size) { + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, (uint8_t) smartair2_protocol::FrameType::CONTROL, message_type, + (uint8_t) smartair2_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); + if (result == haier_protocol::HandlerError::HANDLER_OK) { + result = this->process_status_message_(data, data_size); + if (result != haier_protocol::HandlerError::HANDLER_OK) { + ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); + this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + } else { + if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) { + memcpy(this->last_status_message_.get(), data + 2, sizeof(smartair2_protocol::HaierPacketControl)); + } else { + ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, + sizeof(smartair2_protocol::HaierPacketControl)); + } + if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) { + ESP_LOGI(TAG, "First HVAC status received"); + this->set_phase_(ProtocolPhases::IDLE); + } else if (this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) { + this->set_phase_(ProtocolPhases::IDLE); + } else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) { + this->set_phase_(ProtocolPhases::IDLE); + this->set_force_send_control_(false); + if (this->hvac_settings_.valid) + this->hvac_settings_.reset(); + } + } + return result; + } else { + this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + return result; + } +} + +void Smartair2Climate::set_answers_handlers() { + this->haier_protocol_.set_answer_handler( + (uint8_t) (smartair2_protocol::FrameType::CONTROL), + std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); +} + +void Smartair2Climate::dump_config() { + HaierClimateBase::dump_config(); + ESP_LOGCONFIG(TAG, " Protocol version: smartAir2"); +} + +void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) { + switch (this->protocol_phase_) { + case ProtocolPhases::SENDING_INIT_1: + this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + break; + case ProtocolPhases::WAITING_ANSWER_INIT_1: + case ProtocolPhases::SENDING_INIT_2: + case ProtocolPhases::WAITING_ANSWER_INIT_2: + case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: + case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER: + this->set_phase_(ProtocolPhases::SENDING_INIT_1); + break; + case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: + case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: + case ProtocolPhases::SENDING_SIGNAL_LEVEL: + case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: + this->set_phase_(ProtocolPhases::IDLE); + break; + case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: + if (this->can_send_message() && this->is_protocol_initialisation_interval_exceded_(now)) { + static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL, + 0x4D01); + this->send_message_(STATUS_REQUEST, false); + this->last_status_request_ = now; + this->set_phase_(ProtocolPhases::WAITING_FIRST_STATUS_ANSWER); + } + break; + case ProtocolPhases::SENDING_STATUS_REQUEST: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL, + 0x4D01); + this->send_message_(STATUS_REQUEST, false); + this->last_status_request_ = now; + this->set_phase_(ProtocolPhases::WAITING_STATUS_ANSWER); + } + break; + case ProtocolPhases::SENDING_CONTROL: + if (this->first_control_attempt_) { + this->control_request_timestamp_ = now; + this->first_control_attempt_ = false; + } + if (this->is_control_message_timeout_exceeded_(now)) { + ESP_LOGW(TAG, "Sending control packet timeout!"); + this->set_force_send_control_(false); + if (this->hvac_settings_.valid) + this->hvac_settings_.reset(); + this->forced_request_status_ = true; + this->forced_publish_ = true; + this->set_phase_(ProtocolPhases::IDLE); + } else if (this->can_send_message() && this->is_control_message_interval_exceeded_( + now)) // Using CONTROL_MESSAGES_INTERVAL_MS to speedup requests + { + haier_protocol::HaierMessage control_message = get_control_message(); + this->send_message_(control_message, false); + ESP_LOGI(TAG, "Control packet sent"); + this->set_phase_(ProtocolPhases::WAITING_CONTROL_ANSWER); + } + break; + case ProtocolPhases::SENDING_POWER_ON_COMMAND: + case ProtocolPhases::SENDING_POWER_OFF_COMMAND: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + haier_protocol::HaierMessage power_cmd( + (uint8_t) smartair2_protocol::FrameType::CONTROL, + this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND ? 0x4D02 : 0x4D03); + this->send_message_(power_cmd, false); + this->set_phase_(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND + ? ProtocolPhases::WAITING_POWER_ON_ANSWER + : ProtocolPhases::WAITING_POWER_OFF_ANSWER); + } + break; + case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER: + case ProtocolPhases::WAITING_STATUS_ANSWER: + case ProtocolPhases::WAITING_CONTROL_ANSWER: + case ProtocolPhases::WAITING_POWER_ON_ANSWER: + case ProtocolPhases::WAITING_POWER_OFF_ANSWER: + break; + case ProtocolPhases::IDLE: { + if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { + this->set_phase_(ProtocolPhases::SENDING_STATUS_REQUEST); + this->forced_request_status_ = false; + } + } break; + default: + // Shouldn't get here + ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_); + this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + break; + } +} + +haier_protocol::HaierMessage Smartair2Climate::get_control_message() { + uint8_t control_out_buffer[sizeof(smartair2_protocol::HaierPacketControl)]; + memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(smartair2_protocol::HaierPacketControl)); + smartair2_protocol::HaierPacketControl *out_data = (smartair2_protocol::HaierPacketControl *) control_out_buffer; + out_data->cntrl = 0; + if (this->hvac_settings_.valid) { + HvacSettings climate_control; + climate_control = this->hvac_settings_; + if (climate_control.mode.has_value()) { + switch (climate_control.mode.value()) { + case CLIMATE_MODE_OFF: + out_data->ac_power = 0; + break; + + case CLIMATE_MODE_AUTO: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::AUTO; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + + case CLIMATE_MODE_HEAT: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::HEAT; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + + case CLIMATE_MODE_DRY: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::DRY; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + + case CLIMATE_MODE_FAN_ONLY: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::FAN; + out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode + break; + + case CLIMATE_MODE_COOL: + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::COOL; + out_data->fan_mode = this->other_modes_fan_speed_; + break; + default: + ESP_LOGE("Control", "Unsupported climate mode"); + break; + } + } + // Set fan speed, if we are in fan mode, reject auto in fan mode + if (climate_control.fan_mode.has_value()) { + switch (climate_control.fan_mode.value()) { + case CLIMATE_FAN_LOW: + out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_LOW; + break; + case CLIMATE_FAN_MEDIUM: + out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_MID; + break; + case CLIMATE_FAN_HIGH: + out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_HIGH; + break; + case CLIMATE_FAN_AUTO: + if (this->mode != CLIMATE_MODE_FAN_ONLY) // if we are not in fan only mode + out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_AUTO; + break; + default: + ESP_LOGE("Control", "Unsupported fan mode"); + break; + } + } + // Set swing mode + if (climate_control.swing_mode.has_value()) { + switch (climate_control.swing_mode.value()) { + case CLIMATE_SWING_OFF: + out_data->use_swing_bits = 0; + out_data->swing_both = 0; + break; + case CLIMATE_SWING_VERTICAL: + out_data->swing_both = 0; + out_data->vertical_swing = 1; + out_data->horizontal_swing = 0; + break; + case CLIMATE_SWING_HORIZONTAL: + out_data->swing_both = 0; + out_data->vertical_swing = 0; + out_data->horizontal_swing = 1; + break; + case CLIMATE_SWING_BOTH: + out_data->swing_both = 1; + out_data->use_swing_bits = 0; + out_data->vertical_swing = 0; + out_data->horizontal_swing = 0; + break; + } + } + if (climate_control.target_temperature.has_value()) { + out_data->set_point = + climate_control.target_temperature.value() - 16; // set the temperature at our offset, subtract 16. + } + if (out_data->ac_power == 0) { + // If AC is off - no presets alowed + out_data->turbo_mode = 0; + out_data->quiet_mode = 0; + } else if (climate_control.preset.has_value()) { + switch (climate_control.preset.value()) { + case CLIMATE_PRESET_NONE: + out_data->turbo_mode = 0; + out_data->quiet_mode = 0; + break; + case CLIMATE_PRESET_BOOST: + out_data->turbo_mode = 1; + out_data->quiet_mode = 0; + break; + case CLIMATE_PRESET_COMFORT: + out_data->turbo_mode = 0; + out_data->quiet_mode = 1; + break; + default: + ESP_LOGE("Control", "Unsupported preset"); + out_data->turbo_mode = 0; + out_data->quiet_mode = 0; + break; + } + } + } + out_data->display_status = this->display_status_ ? 0 : 1; + out_data->health_mode = this->health_mode_ ? 1 : 0; + return haier_protocol::HaierMessage((uint8_t) smartair2_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer, + sizeof(smartair2_protocol::HaierPacketControl)); +} + +haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { + if (size < sizeof(smartair2_protocol::HaierStatus)) + return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; + smartair2_protocol::HaierStatus packet; + memcpy(&packet, packet_buffer, size); + bool should_publish = false; + { + // Extra modes/presets + optional old_preset = this->preset; + if (packet.control.turbo_mode != 0) { + this->preset = CLIMATE_PRESET_BOOST; + } else if (packet.control.quiet_mode != 0) { + this->preset = CLIMATE_PRESET_COMFORT; + } else { + this->preset = CLIMATE_PRESET_NONE; + } + should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value()); + } + { + // Target temperature + float old_target_temperature = this->target_temperature; + this->target_temperature = packet.control.set_point + 16.0f; + should_publish = should_publish || (old_target_temperature != this->target_temperature); + } + { + // Current temperature + float old_current_temperature = this->current_temperature; + this->current_temperature = packet.control.room_temperature; + should_publish = should_publish || (old_current_temperature != this->current_temperature); + } + { + // Fan mode + optional old_fan_mode = this->fan_mode; + // remember the fan speed we last had for climate vs fan + if (packet.control.ac_mode == (uint8_t) smartair2_protocol::ConditioningMode::FAN) { + if (packet.control.fan_mode != (uint8_t) smartair2_protocol::FanMode::FAN_AUTO) + this->fan_mode_speed_ = packet.control.fan_mode; + } else { + this->other_modes_fan_speed_ = packet.control.fan_mode; + } + switch (packet.control.fan_mode) { + case (uint8_t) smartair2_protocol::FanMode::FAN_AUTO: + // Somtimes AC reports in fan only mode that fan speed is auto + // but never accept this value back + if (packet.control.ac_mode != (uint8_t) smartair2_protocol::ConditioningMode::FAN) { + this->fan_mode = CLIMATE_FAN_AUTO; + } else { + should_publish = true; + } + break; + case (uint8_t) smartair2_protocol::FanMode::FAN_MID: + this->fan_mode = CLIMATE_FAN_MEDIUM; + break; + case (uint8_t) smartair2_protocol::FanMode::FAN_LOW: + this->fan_mode = CLIMATE_FAN_LOW; + break; + case (uint8_t) smartair2_protocol::FanMode::FAN_HIGH: + this->fan_mode = CLIMATE_FAN_HIGH; + break; + } + should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value()); + } + { + // Display status + // should be before "Climate mode" because it is changing this->mode + if (packet.control.ac_power != 0) { + // if AC is off display status always ON so process it only when AC is on + bool disp_status = packet.control.display_status == 0; + if (disp_status != this->display_status_) { + // Do something only if display status changed + if (this->mode == CLIMATE_MODE_OFF) { + // AC just turned on from remote need to turn off display + this->set_force_send_control_(true); + } else { + this->display_status_ = disp_status; + } + } + } + } + { + // Climate mode + ClimateMode old_mode = this->mode; + if (packet.control.ac_power == 0) { + this->mode = CLIMATE_MODE_OFF; + } else { + // Check current hvac mode + switch (packet.control.ac_mode) { + case (uint8_t) smartair2_protocol::ConditioningMode::COOL: + this->mode = CLIMATE_MODE_COOL; + break; + case (uint8_t) smartair2_protocol::ConditioningMode::HEAT: + this->mode = CLIMATE_MODE_HEAT; + break; + case (uint8_t) smartair2_protocol::ConditioningMode::DRY: + this->mode = CLIMATE_MODE_DRY; + break; + case (uint8_t) smartair2_protocol::ConditioningMode::FAN: + this->mode = CLIMATE_MODE_FAN_ONLY; + break; + case (uint8_t) smartair2_protocol::ConditioningMode::AUTO: + this->mode = CLIMATE_MODE_AUTO; + break; + } + } + should_publish = should_publish || (old_mode != this->mode); + } + { + // Health mode + bool old_health_mode = this->health_mode_; + this->health_mode_ = packet.control.health_mode == 1; + should_publish = should_publish || (old_health_mode != this->health_mode_); + } + { + // Swing mode + ClimateSwingMode old_swing_mode = this->swing_mode; + if (packet.control.swing_both == 0) { + if (packet.control.vertical_swing != 0) { + this->swing_mode = CLIMATE_SWING_VERTICAL; + } else if (packet.control.horizontal_swing != 0) { + this->swing_mode = CLIMATE_SWING_HORIZONTAL; + } else { + this->swing_mode = CLIMATE_SWING_OFF; + } + } else { + swing_mode = CLIMATE_SWING_BOTH; + } + should_publish = should_publish || (old_swing_mode != this->swing_mode); + } + this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); + if (this->forced_publish_ || should_publish) { +#if (HAIER_LOG_LEVEL > 4) + std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now(); +#endif + this->publish_state(); +#if (HAIER_LOG_LEVEL > 4) + ESP_LOGV(TAG, "Publish delay: %lld ms", + std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - + _publish_start) + .count()); +#endif + this->forced_publish_ = false; + } + if (should_publish) { + ESP_LOGI(TAG, "HVAC values changed"); + } + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "HVAC Mode = 0x%X", packet.control.ac_mode); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Fan speed Status = 0x%X", packet.control.fan_mode); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Vertical Swing Status = 0x%X", packet.control.vertical_swing); + esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, + "Set Point Status = 0x%X", packet.control.set_point); + return haier_protocol::HandlerError::HANDLER_OK; +} + +bool Smartair2Climate::is_message_invalid(uint8_t message_type) { + return message_type == (uint8_t) smartair2_protocol::FrameType::INVALID; +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/smartair2_climate.h b/esphome/components/haier/smartair2_climate.h new file mode 100644 index 0000000000..c89d1f0be9 --- /dev/null +++ b/esphome/components/haier/smartair2_climate.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include "haier_base.h" + +namespace esphome { +namespace haier { + +class Smartair2Climate : public HaierClimateBase { + public: + Smartair2Climate(); + Smartair2Climate(const Smartair2Climate &) = delete; + Smartair2Climate &operator=(const Smartair2Climate &) = delete; + ~Smartair2Climate(); + void dump_config() override; + + protected: + void set_answers_handlers() override; + void process_phase(std::chrono::steady_clock::time_point now) override; + haier_protocol::HaierMessage get_control_message() override; + bool is_message_invalid(uint8_t message_type) override; + // Answers handlers + haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data, + size_t data_size); + // Helper functions + haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); + std::unique_ptr last_status_message_; +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/smartair2_packet.h b/esphome/components/haier/smartair2_packet.h new file mode 100644 index 0000000000..8046516c5f --- /dev/null +++ b/esphome/components/haier/smartair2_packet.h @@ -0,0 +1,97 @@ +#pragma once + +namespace esphome { +namespace haier { +namespace smartair2_protocol { + +enum class ConditioningMode : uint8_t { AUTO = 0x00, COOL = 0x01, HEAT = 0x02, FAN = 0x03, DRY = 0x04 }; + +enum class FanMode : uint8_t { FAN_HIGH = 0x00, FAN_MID = 0x01, FAN_LOW = 0x02, FAN_AUTO = 0x03 }; + +struct HaierPacketControl { + // Control bytes starts here + // 10 + uint8_t : 8; // Temperature high byte + // 11 + uint8_t room_temperature; // current room temperature 1°C step + // 12 + uint8_t : 8; // Humidity high byte + // 13 + uint8_t room_humidity; // Humidity 0%-100% with 1% step + // 14 + uint8_t : 8; + // 15 + uint8_t cntrl; // In AC => ESP packets - 0x7F, in ESP => AC packets - 0x00 + // 16 + uint8_t : 8; + // 17 + uint8_t : 8; + // 18 + uint8_t : 8; + // 19 + uint8_t : 8; + // 20 + uint8_t : 8; + // 21 + uint8_t ac_mode; // See enum ConditioningMode + // 22 + uint8_t : 8; + // 23 + uint8_t fan_mode; // See enum FanMode + // 24 + uint8_t : 8; + // 25 + uint8_t swing_both; // If 1 - swing both direction, if 0 - horizontal_swing and vertical_swing define + // vertical/horizontal/off + // 26 + uint8_t : 3; + uint8_t use_fahrenheit : 1; + uint8_t : 3; + uint8_t lock_remote : 1; // Disable remote + // 27 + uint8_t ac_power : 1; // Is ac on or off + uint8_t : 2; + uint8_t health_mode : 1; // Health mode on or off + uint8_t compressor : 1; // Compressor on or off ??? + uint8_t : 1; + uint8_t ten_degree : 1; // 10 degree status (only work in heat mode) + uint8_t : 0; + // 28 + uint8_t : 8; + // 29 + uint8_t use_swing_bits : 1; // Indicate if horizontal_swing and vertical_swing should be used + uint8_t turbo_mode : 1; // Turbo mode + uint8_t quiet_mode : 1; // Sleep mode + uint8_t horizontal_swing : 1; // Horizontal swing (if swing_both == 0) + uint8_t vertical_swing : 1; // Vertical swing (if swing_both == 0) if vertical_swing and horizontal_swing both 0 => + // swing off + uint8_t display_status : 1; // Led on or off + uint8_t : 0; + // 30 + uint8_t : 8; + // 31 + uint8_t : 8; + // 32 + uint8_t : 8; // Target temperature high byte + // 33 + uint8_t set_point; // Target temperature with 16°C offset, 1°C step +}; + +struct HaierStatus { + uint16_t subcommand; + HaierPacketControl control; +}; + +enum class FrameType : uint8_t { + CONTROL = 0x01, + STATUS = 0x02, + INVALID = 0x03, + CONFIRM = 0x05, + GET_DEVICE_VERSION = 0x61, + REPORT_NETWORK_STATUS = 0xF7, + NO_COMMAND = 0xFF, +}; + +} // namespace smartair2_protocol +} // namespace haier +} // namespace esphome diff --git a/esphome/components/havells_solar/havells_solar.h b/esphome/components/havells_solar/havells_solar.h index 2ccc8be3d4..f3ac8fafcf 100644 --- a/esphome/components/havells_solar/havells_solar.h +++ b/esphome/components/havells_solar/havells_solar.h @@ -4,6 +4,8 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/modbus/modbus.h" +#include + namespace esphome { namespace havells_solar { diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 52d2b3d8b7..44cf5ae049 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -1,5 +1,4 @@ #include "hbridge_fan.h" -#include "esphome/components/fan/fan_helpers.h" #include "esphome/core/log.h" namespace esphome { diff --git a/esphome/components/hdc1080/hdc1080.h b/esphome/components/hdc1080/hdc1080.h index 9cb87cdb8b..2ff7b6dc33 100644 --- a/esphome/components/hdc1080/hdc1080.h +++ b/esphome/components/hdc1080/hdc1080.h @@ -21,8 +21,8 @@ class HDC1080Component : public PollingComponent, public i2c::I2CDevice { float get_setup_priority() const override; protected: - sensor::Sensor *temperature_; - sensor::Sensor *humidity_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; }; } // namespace hdc1080 diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 744ef5e527..cc8e75dcbd 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_PROTOCOL, CONF_VISUAL, ) +from esphome.core import CORE CODEOWNERS = ["@rob-deutsch"] @@ -25,6 +26,7 @@ PROTOCOLS = { "daikin_arc417": Protocol.PROTOCOL_DAIKIN_ARC417, "daikin_arc480": Protocol.PROTOCOL_DAIKIN_ARC480, "daikin": Protocol.PROTOCOL_DAIKIN, + "electroluxyal": Protocol.PROTOCOL_ELECTROLUXYAL, "fuego": Protocol.PROTOCOL_FUEGO, "fujitsu_awyz": Protocol.PROTOCOL_FUJITSU_AWYZ, "gree": Protocol.PROTOCOL_GREE, @@ -57,6 +59,7 @@ PROTOCOLS = { "sharp": Protocol.PROTOCOL_SHARP, "toshiba_daiseikai": Protocol.PROTOCOL_TOSHIBA_DAISEIKAI, "toshiba": Protocol.PROTOCOL_TOSHIBA, + "zhlt01": Protocol.PROTOCOL_ZHLT01, } CONF_HORIZONTAL_DEFAULT = "horizontal_default" @@ -112,6 +115,7 @@ def to_code(config): cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) - # PIO isn't updating releases, so referencing the release tag directly. See: - # https://github.com/ToniA/arduino-heatpumpir/commit/0948c619d86407a4e50e8db2f3c193e0576c86fd - cg.add_library("", "", "https://github.com/ToniA/arduino-heatpumpir.git#1.0.18") + cg.add_library("tonia/HeatpumpIR", "1.0.20") + + if CORE.is_esp8266 or CORE.is_esp32: + cg.add_library("crankyoldgit/IRremoteESP8266", "2.7.12") diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp index ad3731b955..bed1dc76c0 100644 --- a/esphome/components/heatpumpir/heatpumpir.cpp +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -20,6 +20,7 @@ const std::map> PROTOCOL_CONSTRUCTOR_MAP {PROTOCOL_DAIKIN_ARC417, []() { return new DaikinHeatpumpARC417IR(); }}, // NOLINT {PROTOCOL_DAIKIN_ARC480, []() { return new DaikinHeatpumpARC480A14IR(); }}, // NOLINT {PROTOCOL_DAIKIN, []() { return new DaikinHeatpumpIR(); }}, // NOLINT + {PROTOCOL_ELECTROLUXYAL, []() { return new ElectroluxYALHeatpumpIR(); }}, // NOLINT {PROTOCOL_FUEGO, []() { return new FuegoHeatpumpIR(); }}, // NOLINT {PROTOCOL_FUJITSU_AWYZ, []() { return new FujitsuHeatpumpIR(); }}, // NOLINT {PROTOCOL_GREE, []() { return new GreeGenericHeatpumpIR(); }}, // NOLINT @@ -52,6 +53,7 @@ const std::map> PROTOCOL_CONSTRUCTOR_MAP {PROTOCOL_SHARP, []() { return new SharpHeatpumpIR(); }}, // NOLINT {PROTOCOL_TOSHIBA_DAISEIKAI, []() { return new ToshibaDaiseikaiHeatpumpIR(); }}, // NOLINT {PROTOCOL_TOSHIBA, []() { return new ToshibaHeatpumpIR(); }}, // NOLINT + {PROTOCOL_ZHLT01, []() { return new ZHLT01HeatpumpIR(); }}, // NOLINT }; void HeatpumpIRClimate::setup() { diff --git a/esphome/components/heatpumpir/heatpumpir.h b/esphome/components/heatpumpir/heatpumpir.h index 18d9b5040f..c60b944111 100644 --- a/esphome/components/heatpumpir/heatpumpir.h +++ b/esphome/components/heatpumpir/heatpumpir.h @@ -20,6 +20,7 @@ enum Protocol { PROTOCOL_DAIKIN_ARC417, PROTOCOL_DAIKIN_ARC480, PROTOCOL_DAIKIN, + PROTOCOL_ELECTROLUXYAL, PROTOCOL_FUEGO, PROTOCOL_FUJITSU_AWYZ, PROTOCOL_GREE, @@ -52,6 +53,7 @@ enum Protocol { PROTOCOL_SHARP, PROTOCOL_TOSHIBA_DAISEIKAI, PROTOCOL_TOSHIBA, + PROTOCOL_ZHLT01, }; // Simple enum to represent horizontal directios diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.cpp b/esphome/components/hitachi_ac344/hitachi_ac344.cpp index 7b93b00503..2825e4f04c 100644 --- a/esphome/components/hitachi_ac344/hitachi_ac344.cpp +++ b/esphome/components/hitachi_ac344/hitachi_ac344.cpp @@ -12,7 +12,7 @@ void set_bits(uint8_t *const dst, const uint8_t offset, const uint8_t nbits, con uint8_t mask = UINT8_MAX >> (8 - ((nbits > 8) ? 8 : nbits)); // Calculate the mask & clear the space for the data. // Clear the destination bits. - *dst &= ~(uint8_t)(mask << offset); + *dst &= ~(uint8_t) (mask << offset); // Merge in the data. *dst |= ((data & mask) << offset); } diff --git a/esphome/components/hitachi_ac424/hitachi_ac424.cpp b/esphome/components/hitachi_ac424/hitachi_ac424.cpp index 65cfaa4175..0bfc3ae564 100644 --- a/esphome/components/hitachi_ac424/hitachi_ac424.cpp +++ b/esphome/components/hitachi_ac424/hitachi_ac424.cpp @@ -12,7 +12,7 @@ void set_bits(uint8_t *const dst, const uint8_t offset, const uint8_t nbits, con uint8_t mask = UINT8_MAX >> (8 - ((nbits > 8) ? 8 : nbits)); // Calculate the mask & clear the space for the data. // Clear the destination bits. - *dst &= ~(uint8_t)(mask << offset); + *dst &= ~(uint8_t) (mask << offset); // Merge in the data. *dst |= ((data & mask) << offset); } diff --git a/esphome/components/hlw8012/hlw8012.h b/esphome/components/hlw8012/hlw8012.h index 5060957cf1..adb49ffb66 100644 --- a/esphome/components/hlw8012/hlw8012.h +++ b/esphome/components/hlw8012/hlw8012.h @@ -16,8 +16,17 @@ enum HLW8012SensorModels { HLW8012_SENSOR_MODEL_BL0937 }; +#ifdef HAS_PCNT +#define USE_PCNT true +#else +#define USE_PCNT false +#endif + class HLW8012Component : public PollingComponent { public: + HLW8012Component() + : cf_store_(*pulse_counter::get_storage(USE_PCNT)), cf1_store_(*pulse_counter::get_storage(USE_PCNT)) {} + void setup() override; void dump_config() override; float get_setup_priority() const override; @@ -49,9 +58,9 @@ class HLW8012Component : public PollingComponent { uint64_t cf_total_pulses_{0}; GPIOPin *sel_pin_; InternalGPIOPin *cf_pin_; - pulse_counter::PulseCounterStorage cf_store_; + pulse_counter::PulseCounterStorageBase &cf_store_; InternalGPIOPin *cf1_pin_; - pulse_counter::PulseCounterStorage cf1_store_; + pulse_counter::PulseCounterStorageBase &cf1_store_; sensor::Sensor *voltage_sensor_{nullptr}; sensor::Sensor *current_sensor_{nullptr}; sensor::Sensor *power_sensor_{nullptr}; diff --git a/esphome/components/hm3301/abstract_aqi_calculator.h b/esphome/components/hm3301/abstract_aqi_calculator.h index 42d900a262..038828e9de 100644 --- a/esphome/components/hm3301/abstract_aqi_calculator.h +++ b/esphome/components/hm3301/abstract_aqi_calculator.h @@ -7,7 +7,7 @@ namespace hm3301 { class AbstractAQICalculator { public: - virtual uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0; + virtual uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0; }; } // namespace hm3301 diff --git a/esphome/components/hm3301/aqi_calculator.h b/esphome/components/hm3301/aqi_calculator.h index 08d1dc2921..6c830f9bad 100644 --- a/esphome/components/hm3301/aqi_calculator.h +++ b/esphome/components/hm3301/aqi_calculator.h @@ -7,7 +7,7 @@ namespace hm3301 { class AQICalculator : public AbstractAQICalculator { public: - uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { + uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); diff --git a/esphome/components/hm3301/caqi_calculator.h b/esphome/components/hm3301/caqi_calculator.h index 1ec61f2416..3f338776d8 100644 --- a/esphome/components/hm3301/caqi_calculator.h +++ b/esphome/components/hm3301/caqi_calculator.h @@ -8,7 +8,7 @@ namespace hm3301 { class CAQICalculator : public AbstractAQICalculator { public: - uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { + uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp index a2bef2a01d..379c4dbc5a 100644 --- a/esphome/components/hm3301/hm3301.cpp +++ b/esphome/components/hm3301/hm3301.cpp @@ -62,7 +62,7 @@ void HM3301Component::update() { pm_10_0_value = get_sensor_value_(data_buffer_, PM_10_0_VALUE_INDEX); } - int8_t aqi_value = -1; + int16_t aqi_value = -1; if (this->aqi_sensor_ != nullptr && pm_2_5_value != -1 && pm_10_0_value != -1) { AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_); aqi_value = calculator->get_aqi(pm_2_5_value, pm_10_0_value); diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py index 8e9ee4c6fb..27af0b5b6b 100644 --- a/esphome/components/hm3301/sensor.py +++ b/esphome/components/hm3301/sensor.py @@ -16,6 +16,7 @@ from esphome.const import ( ) DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@freekode"] hm3301_ns = cg.esphome_ns.namespace("hm3301") HM3301Component = hm3301_ns.class_( diff --git a/esphome/components/hmc5883l/hmc5883l.h b/esphome/components/hmc5883l/hmc5883l.h index 41d41baa22..3481f45dc8 100644 --- a/esphome/components/hmc5883l/hmc5883l.h +++ b/esphome/components/hmc5883l/hmc5883l.h @@ -54,10 +54,10 @@ class HMC5883LComponent : public PollingComponent, public i2c::I2CDevice { HMC5883LOversampling oversampling_{HMC5883L_OVERSAMPLING_1}; HMC5883LDatarate datarate_{HMC5883L_DATARATE_15_0_HZ}; HMC5883LRange range_{HMC5883L_RANGE_130_UT}; - sensor::Sensor *x_sensor_; - sensor::Sensor *y_sensor_; - sensor::Sensor *z_sensor_; - sensor::Sensor *heading_sensor_; + sensor::Sensor *x_sensor_{nullptr}; + sensor::Sensor *y_sensor_{nullptr}; + sensor::Sensor *z_sensor_{nullptr}; + sensor::Sensor *heading_sensor_{nullptr}; enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, diff --git a/esphome/components/hmc5883l/sensor.py b/esphome/components/hmc5883l/sensor.py index 9d8701079e..26e8e2b60c 100644 --- a/esphome/components/hmc5883l/sensor.py +++ b/esphome/components/hmc5883l/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_RANGE, ICON_MAGNET, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, UNIT_MICROTESLA, UNIT_DEGREES, ICON_SCREEN_ROTATION, @@ -88,7 +87,6 @@ heading_schema = sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, icon=ICON_SCREEN_ROTATION, accuracy_decimals=1, - state_class=STATE_CLASS_NONE, ) CONFIG_SCHEMA = ( diff --git a/esphome/components/homeassistant/__init__.py b/esphome/components/homeassistant/__init__.py index c151abc250..776aa7fd7b 100644 --- a/esphome/components/homeassistant/__init__.py +++ b/esphome/components/homeassistant/__init__.py @@ -1,4 +1,20 @@ import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_INTERNAL CODEOWNERS = ["@OttoWinter"] homeassistant_ns = cg.esphome_ns.namespace("homeassistant") + +HOME_ASSISTANT_IMPORT_SCHEMA = cv.Schema( + { + cv.Required(CONF_ENTITY_ID): cv.entity_id, + cv.Optional(CONF_ATTRIBUTE): cv.string, + cv.Optional(CONF_INTERNAL, default=True): cv.boolean, + } +) + + +def setup_home_assistant_entity(var, config): + cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) + if CONF_ATTRIBUTE in config: + cg.add(var.set_attribute(config[CONF_ATTRIBUTE])) diff --git a/esphome/components/homeassistant/binary_sensor/__init__.py b/esphome/components/homeassistant/binary_sensor/__init__.py index 4972466aac..a943368dd7 100644 --- a/esphome/components/homeassistant/binary_sensor/__init__.py +++ b/esphome/components/homeassistant/binary_sensor/__init__.py @@ -1,28 +1,24 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_ID -from .. import homeassistant_ns + +from .. import ( + HOME_ASSISTANT_IMPORT_SCHEMA, + homeassistant_ns, + setup_home_assistant_entity, +) DEPENDENCIES = ["api"] + HomeassistantBinarySensor = homeassistant_ns.class_( "HomeassistantBinarySensor", binary_sensor.BinarySensor, cg.Component ) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(HomeassistantBinarySensor), - cv.Required(CONF_ENTITY_ID): cv.entity_id, - cv.Optional(CONF_ATTRIBUTE): cv.string, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(HomeassistantBinarySensor).extend( + HOME_ASSISTANT_IMPORT_SCHEMA +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) - await binary_sensor.register_binary_sensor(var, config) - - cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) - if CONF_ATTRIBUTE in config: - cg.add(var.set_attribute(config[CONF_ATTRIBUTE])) + setup_home_assistant_entity(var, config) diff --git a/esphome/components/homeassistant/sensor/__init__.py b/esphome/components/homeassistant/sensor/__init__.py index cf29db8bb8..6437476827 100644 --- a/esphome/components/homeassistant/sensor/__init__.py +++ b/esphome/components/homeassistant/sensor/__init__.py @@ -1,13 +1,11 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor -from esphome.const import ( - CONF_ATTRIBUTE, - CONF_ENTITY_ID, - CONF_ID, - STATE_CLASS_NONE, + +from .. import ( + HOME_ASSISTANT_IMPORT_SCHEMA, + homeassistant_ns, + setup_home_assistant_entity, ) -from .. import homeassistant_ns DEPENDENCIES = ["api"] @@ -15,23 +13,12 @@ HomeassistantSensor = homeassistant_ns.class_( "HomeassistantSensor", sensor.Sensor, cg.Component ) -CONFIG_SCHEMA = sensor.sensor_schema( - accuracy_decimals=1, - state_class=STATE_CLASS_NONE, -).extend( - { - cv.GenerateID(): cv.declare_id(HomeassistantSensor), - cv.Required(CONF_ENTITY_ID): cv.entity_id, - cv.Optional(CONF_ATTRIBUTE): cv.string, - } +CONFIG_SCHEMA = sensor.sensor_schema(HomeassistantSensor, accuracy_decimals=1).extend( + HOME_ASSISTANT_IMPORT_SCHEMA ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) - - cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) - if CONF_ATTRIBUTE in config: - cg.add(var.set_attribute(config[CONF_ATTRIBUTE])) + setup_home_assistant_entity(var, config) diff --git a/esphome/components/homeassistant/text_sensor/__init__.py b/esphome/components/homeassistant/text_sensor/__init__.py index b63d45b9ce..b59f9d23df 100644 --- a/esphome/components/homeassistant/text_sensor/__init__.py +++ b/esphome/components/homeassistant/text_sensor/__init__.py @@ -1,8 +1,11 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import text_sensor -from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_ID -from .. import homeassistant_ns + +from .. import ( + HOME_ASSISTANT_IMPORT_SCHEMA, + homeassistant_ns, + setup_home_assistant_entity, +) DEPENDENCIES = ["api"] @@ -10,20 +13,12 @@ HomeassistantTextSensor = homeassistant_ns.class_( "HomeassistantTextSensor", text_sensor.TextSensor, cg.Component ) -CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(HomeassistantTextSensor), - cv.Required(CONF_ENTITY_ID): cv.entity_id, - cv.Optional(CONF_ATTRIBUTE): cv.string, - } +CONFIG_SCHEMA = text_sensor.text_sensor_schema(HomeassistantTextSensor).extend( + HOME_ASSISTANT_IMPORT_SCHEMA ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await text_sensor.new_text_sensor(config) await cg.register_component(var, config) - await text_sensor.register_text_sensor(var, config) - - cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) - if CONF_ATTRIBUTE in config: - cg.add(var.set_attribute(config[CONF_ATTRIBUTE])) + setup_home_assistant_entity(var, config) diff --git a/esphome/components/honeywellabp/__init__.py b/esphome/components/honeywellabp/__init__.py new file mode 100644 index 0000000000..03440d675d --- /dev/null +++ b/esphome/components/honeywellabp/__init__.py @@ -0,0 +1 @@ +"""Support for Honeywell ABP""" diff --git a/esphome/components/honeywellabp/honeywellabp.cpp b/esphome/components/honeywellabp/honeywellabp.cpp new file mode 100644 index 0000000000..124bd6bb95 --- /dev/null +++ b/esphome/components/honeywellabp/honeywellabp.cpp @@ -0,0 +1,102 @@ +#include "honeywellabp.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace honeywellabp { + +static const char *const TAG = "honeywellabp"; + +const float MIN_COUNT = 1638.4; // 1638 counts (10% of 2^14 counts or 0x0666) +const float MAX_COUNT = 14745.6; // 14745 counts (90% of 2^14 counts or 0x3999) + +void HONEYWELLABPSensor::setup() { + ESP_LOGD(TAG, "Setting up Honeywell ABP Sensor "); + this->spi_setup(); +} + +uint8_t HONEYWELLABPSensor::readsensor_() { + // Polls the sensor for new data. + // transfer 4 bytes (the last two are temperature only used by some sensors) + this->enable(); + buf_[0] = this->read_byte(); + buf_[1] = this->read_byte(); + buf_[2] = this->read_byte(); + buf_[3] = this->read_byte(); + this->disable(); + + // Check the status codes: + // status = 0 : normal operation + // status = 1 : device in command mode + // status = 2 : stale data + // status = 3 : diagnostic condition + status_ = buf_[0] >> 6 & 0x3; + ESP_LOGV(TAG, "Sensor status %d", status_); + + // if device is normal and there is new data, bitmask and save the raw data + if (status_ == 0) { + // 14 - bit pressure is the last 6 bits of byte 0 (high bits) & all of byte 1 (lowest 8 bits) + pressure_count_ = ((uint16_t) (buf_[0]) << 8 & 0x3F00) | ((uint16_t) (buf_[1]) & 0xFF); + // 11 - bit temperature is all of byte 2 (lowest 8 bits) and the first three bits of byte 3 + temperature_count_ = (((uint16_t) (buf_[2]) << 3) & 0x7F8) | (((uint16_t) (buf_[3]) >> 5) & 0x7); + ESP_LOGV(TAG, "Sensor pressure_count_ %d", pressure_count_); + ESP_LOGV(TAG, "Sensor temperature_count_ %d", temperature_count_); + } + return status_; +} + +// returns status +uint8_t HONEYWELLABPSensor::readstatus_() { return status_; } + +// The pressure value from the most recent reading in raw counts +int HONEYWELLABPSensor::rawpressure_() { return pressure_count_; } + +// The temperature value from the most recent reading in raw counts +int HONEYWELLABPSensor::rawtemperature_() { return temperature_count_; } + +// Converts a digital pressure measurement in counts to pressure measured +float HONEYWELLABPSensor::countstopressure_(const int counts, const float min_pressure, const float max_pressure) { + return ((((float) counts - MIN_COUNT) * (max_pressure - min_pressure)) / (MAX_COUNT - MIN_COUNT)) + min_pressure; +} + +// Converts a digital temperature measurement in counts to temperature in C +// This will be invalid if sensore daoes not have temperature measurement capability +float HONEYWELLABPSensor::countstotemperatures_(const int counts) { return (((float) counts / 2047.0) * 200.0) - 50.0; } + +// Pressure value from the most recent reading in units +float HONEYWELLABPSensor::read_pressure_() { + return countstopressure_(pressure_count_, honeywellabp_min_pressure_, honeywellabp_max_pressure_); +} + +// Temperature value from the most recent reading in degrees C +float HONEYWELLABPSensor::read_temperature_() { return countstotemperatures_(temperature_count_); } + +void HONEYWELLABPSensor::update() { + ESP_LOGV(TAG, "Update Honeywell ABP Sensor"); + if (readsensor_() == 0) { + if (this->pressure_sensor_ != nullptr) + this->pressure_sensor_->publish_state(read_pressure_() * 1.0); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(read_temperature_() * 1.0); + } +} + +float HONEYWELLABPSensor::get_setup_priority() const { return setup_priority::LATE; } + +void HONEYWELLABPSensor::dump_config() { + // LOG_SENSOR("", "HONEYWELLABP", this); + LOG_PIN(" CS Pin: ", this->cs_); + ESP_LOGCONFIG(TAG, " Min Pressure Range: %0.1f", honeywellabp_min_pressure_); + ESP_LOGCONFIG(TAG, " Max Pressure Range: %0.1f", honeywellabp_max_pressure_); + LOG_UPDATE_INTERVAL(this); +} + +void HONEYWELLABPSensor::set_honeywellabp_min_pressure(float min_pressure) { + this->honeywellabp_min_pressure_ = min_pressure; +} + +void HONEYWELLABPSensor::set_honeywellabp_max_pressure(float max_pressure) { + this->honeywellabp_max_pressure_ = max_pressure; +} + +} // namespace honeywellabp +} // namespace esphome diff --git a/esphome/components/honeywellabp/honeywellabp.h b/esphome/components/honeywellabp/honeywellabp.h new file mode 100644 index 0000000000..98f6f08c4a --- /dev/null +++ b/esphome/components/honeywellabp/honeywellabp.h @@ -0,0 +1,45 @@ +// for Honeywell ABP sensor +// adapting code from https://github.com/vwls/Honeywell_pressure_sensors +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/spi/spi.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace honeywellabp { + +class HONEYWELLABPSensor : public PollingComponent, + public spi::SPIDevice { + public: + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void setup() override; + void update() override; + float get_setup_priority() const override; + void dump_config() override; + void set_honeywellabp_min_pressure(float min_pressure); + void set_honeywellabp_max_pressure(float max_pressure); + + protected: + float honeywellabp_min_pressure_ = 0.0; + float honeywellabp_max_pressure_ = 0.0; + uint8_t buf_[4]; // buffer to hold sensor data + uint8_t status_ = 0; // byte to hold status information. + int pressure_count_ = 0; // hold raw pressure data (14 - bit, 0 - 16384) + int temperature_count_ = 0; // hold raw temperature data (11 - bit, 0 - 2048) + sensor::Sensor *pressure_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + uint8_t readsensor_(); + uint8_t readstatus_(); + int rawpressure_(); + int rawtemperature_(); + float countstopressure_(int counts, float min_pressure, float max_pressure); + float countstotemperatures_(int counts); + float read_pressure_(); + float read_temperature_(); +}; + +} // namespace honeywellabp +} // namespace esphome diff --git a/esphome/components/honeywellabp/sensor.py b/esphome/components/honeywellabp/sensor.py new file mode 100644 index 0000000000..ed8bff6e9b --- /dev/null +++ b/esphome/components/honeywellabp/sensor.py @@ -0,0 +1,69 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.components import spi +from esphome.const import ( + CONF_ID, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +DEPENDENCIES = ["spi"] +CODEOWNERS = ["@RubyBailey"] + +CONF_MIN_PRESSURE = "min_pressure" +CONF_MAX_PRESSURE = "max_pressure" + +honeywellabp_ns = cg.esphome_ns.namespace("honeywellabp") +HONEYWELLABPSensor = honeywellabp_ns.class_( + "HONEYWELLABPSensor", sensor.Sensor, cg.PollingComponent, spi.SPIDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HONEYWELLABPSensor), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement="psi", + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Required(CONF_MIN_PRESSURE): cv.float_, + cv.Required(CONF_MAX_PRESSURE): cv.float_, + } + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(spi.spi_device_schema(cs_pin_required=True)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) + + if CONF_PRESSURE in config: + conf = config[CONF_PRESSURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_pressure_sensor(sens)) + cg.add(var.set_honeywellabp_min_pressure(conf[CONF_MIN_PRESSURE])) + cg.add(var.set_honeywellabp_max_pressure(conf[CONF_MAX_PRESSURE])) + + if CONF_TEMPERATURE in config: + conf = config[CONF_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_temperature_sensor(sens)) diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py new file mode 100644 index 0000000000..46f763d255 --- /dev/null +++ b/esphome/components/host/__init__.py @@ -0,0 +1,38 @@ +from esphome.const import ( + KEY_CORE, + KEY_FRAMEWORK_VERSION, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, +) +from esphome.core import CORE +import esphome.config_validation as cv +import esphome.codegen as cg + +from .const import KEY_HOST + +# force import gpio to register pin schema +from .gpio import host_pin_to_code # noqa + + +CODEOWNERS = ["@esphome/core"] +AUTO_LOAD = ["network"] + + +def set_core_data(config): + CORE.data[KEY_HOST] = {} + CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = "host" + CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "host" + CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version(1, 0, 0) + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema({}), + set_core_data, +) + + +async def to_code(config): + cg.add_build_flag("-DUSE_HOST") + cg.add_define("ESPHOME_BOARD", "host") + cg.add_platformio_option("platform", "platformio/native") diff --git a/esphome/components/host/const.py b/esphome/components/host/const.py new file mode 100644 index 0000000000..b6f4c4e277 --- /dev/null +++ b/esphome/components/host/const.py @@ -0,0 +1,5 @@ +import esphome.codegen as cg + +KEY_HOST = "host" + +host_ns = cg.esphome_ns.namespace("host") diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp new file mode 100644 index 0000000000..164d622dd4 --- /dev/null +++ b/esphome/components/host/core.cpp @@ -0,0 +1,77 @@ +#ifdef USE_HOST + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "preferences.h" + +#include +#include +#include +#include + +namespace esphome { + +void IRAM_ATTR HOT yield() { ::sched_yield(); } +uint32_t IRAM_ATTR HOT millis() { + struct timespec spec; + clock_gettime(CLOCK_MONOTONIC, &spec); + time_t seconds = spec.tv_sec; + uint32_t ms = round(spec.tv_nsec / 1e6); + return ((uint32_t) seconds) * 1000U + ms; +} +void IRAM_ATTR HOT delay(uint32_t ms) { + struct timespec ts; + ts.tv_sec = ms / 1000; + ts.tv_nsec = (ms % 1000) * 1000000; + int res; + do { + res = nanosleep(&ts, &ts); + } while (res != 0 && errno == EINTR); +} +uint32_t IRAM_ATTR HOT micros() { + struct timespec spec; + clock_gettime(CLOCK_MONOTONIC, &spec); + time_t seconds = spec.tv_sec; + uint32_t us = round(spec.tv_nsec / 1e3); + return ((uint32_t) seconds) * 1000000U + us; +} +void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { + struct timespec ts; + ts.tv_sec = us / 1000000U; + ts.tv_nsec = (us % 1000000U) * 1000U; + int res; + do { + res = nanosleep(&ts, &ts); + } while (res != 0 && errno == EINTR); +} +void arch_restart() { exit(0); } +void arch_init() { + // pass +} +void IRAM_ATTR HOT arch_feed_wdt() { + // pass +} + +uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +uint32_t arch_get_cpu_cycle_count() { + struct timespec spec; + clock_gettime(CLOCK_MONOTONIC, &spec); + time_t seconds = spec.tv_sec; + uint32_t us = spec.tv_nsec; + return ((uint32_t) seconds) * 1000000000U + us; +} +uint32_t arch_get_cpu_freq_hz() { return 1000000000U; } + +} // namespace esphome + +void setup(); +void loop(); +int main() { + esphome::host::setup_preferences(); + setup(); + while (true) { + loop(); + } +} + +#endif // USE_HOST diff --git a/esphome/components/host/gpio.cpp b/esphome/components/host/gpio.cpp new file mode 100644 index 0000000000..e46f158513 --- /dev/null +++ b/esphome/components/host/gpio.cpp @@ -0,0 +1,59 @@ +#ifdef USE_HOST + +#include "gpio.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace host { + +static const char *const TAG = "host"; + +struct ISRPinArg { + uint8_t pin; + bool inverted; +}; + +ISRInternalGPIOPin HostGPIOPin::to_isr() const { + auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) + arg->pin = pin_; + arg->inverted = inverted_; + return ISRInternalGPIOPin((void *) arg); +} + +void HostGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { + ESP_LOGD(TAG, "Attaching interrupt %p to pin %d and mode %d", func, pin_, (uint32_t) type); +} +void HostGPIOPin::pin_mode(gpio::Flags flags) { ESP_LOGD(TAG, "Setting pin %d mode to %02X", pin_, (uint32_t) flags); } + +std::string HostGPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "GPIO%u", pin_); + return buffer; +} + +bool HostGPIOPin::digital_read() { return inverted_; } +void HostGPIOPin::digital_write(bool value) { + // pass + ESP_LOGD(TAG, "Setting pin %d to %s", pin_, value != inverted_ ? "HIGH" : "LOW"); +} +void HostGPIOPin::detach_interrupt() const {} + +} // namespace host + +using namespace host; + +bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { + auto *arg = reinterpret_cast(arg_); + return arg->inverted; +} +void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { + // pass +} +void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { + auto *arg = reinterpret_cast(arg_); + ESP_LOGD(TAG, "Clearing interrupt for pin %d", arg->pin); +} + +} // namespace esphome + +#endif // USE_HOST diff --git a/esphome/components/esp32/gpio_arduino.h b/esphome/components/host/gpio.h similarity index 83% rename from esphome/components/esp32/gpio_arduino.h rename to esphome/components/host/gpio.h index e88d39b1a8..c0920467d6 100644 --- a/esphome/components/esp32/gpio_arduino.h +++ b/esphome/components/host/gpio.h @@ -1,12 +1,13 @@ #pragma once -#ifdef USE_ESP32_FRAMEWORK_ARDUINO +#ifdef USE_HOST + #include "esphome/core/hal.h" namespace esphome { -namespace esp32 { +namespace host { -class ArduinoInternalGPIOPin : public InternalGPIOPin { +class HostGPIOPin : public InternalGPIOPin { public: void set_pin(uint8_t pin) { pin_ = pin; } void set_inverted(bool inverted) { inverted_ = inverted; } @@ -30,7 +31,7 @@ class ArduinoInternalGPIOPin : public InternalGPIOPin { gpio::Flags flags_; }; -} // namespace esp32 +} // namespace host } // namespace esphome -#endif // USE_ESP32_FRAMEWORK_ARDUINO +#endif // USE_HOST diff --git a/esphome/components/host/gpio.py b/esphome/components/host/gpio.py new file mode 100644 index 0000000000..d523d28ee5 --- /dev/null +++ b/esphome/components/host/gpio.py @@ -0,0 +1,73 @@ +import logging + +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OPEN_DRAIN, + CONF_OUTPUT, + CONF_PULLDOWN, + CONF_PULLUP, +) +from esphome import pins +import esphome.config_validation as cv +import esphome.codegen as cg + +from .const import host_ns + + +_LOGGER = logging.getLogger(__name__) + + +HostGPIOPin = host_ns.class_("HostGPIOPin", cg.InternalGPIOPin) + + +def _translate_pin(value): + if isinstance(value, dict) or value is None: + raise cv.Invalid( + "This variable only supports pin numbers, not full pin schemas " + "(with inverted and mode)." + ) + if isinstance(value, int): + return value + try: + return int(value) + except ValueError: + pass + if value.startswith("GPIO"): + return cv.int_(value[len("GPIO") :].strip()) + return value + + +def validate_gpio_pin(value): + return _translate_pin(value) + + +HOST_PIN_SCHEMA = cv.All( + { + cv.GenerateID(): cv.declare_id(HostGPIOPin), + cv.Required(CONF_NUMBER): validate_gpio_pin, + cv.Optional(CONF_MODE, default={}): cv.Schema( + { + cv.Optional(CONF_INPUT, default=False): cv.boolean, + cv.Optional(CONF_OUTPUT, default=False): cv.boolean, + cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, + cv.Optional(CONF_PULLUP, default=False): cv.boolean, + cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, + } + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + }, +) + + +@pins.PIN_SCHEMA_REGISTRY.register("host", HOST_PIN_SCHEMA) +async def host_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/host/preferences.cpp b/esphome/components/host/preferences.cpp new file mode 100644 index 0000000000..bf45893e40 --- /dev/null +++ b/esphome/components/host/preferences.cpp @@ -0,0 +1,36 @@ +#ifdef USE_HOST + +#include "preferences.h" +#include +#include "esphome/core/preferences.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/defines.h" + +namespace esphome { +namespace host { + +static const char *const TAG = "host.preferences"; + +class HostPreferences : public ESPPreferences { + public: + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { return {}; } + + ESPPreferenceObject make_preference(size_t length, uint32_t type) override { return {}; } + + bool sync() override { return true; } + bool reset() override { return true; } +}; + +void setup_preferences() { + auto *pref = new HostPreferences(); // NOLINT(cppcoreguidelines-owning-memory) + global_preferences = pref; +} + +} // namespace host + +ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome + +#endif // USE_HOST diff --git a/esphome/components/host/preferences.h b/esphome/components/host/preferences.h new file mode 100644 index 0000000000..7462360ec3 --- /dev/null +++ b/esphome/components/host/preferences.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef USE_HOST + +namespace esphome { +namespace host { + +void setup_preferences(); + +} // namespace host +} // namespace esphome + +#endif // USE_HOST diff --git a/esphome/components/hrxl_maxsonar_wr/sensor.py b/esphome/components/hrxl_maxsonar_wr/sensor.py index dd43bd84a7..a78ae574b1 100644 --- a/esphome/components/hrxl_maxsonar_wr/sensor.py +++ b/esphome/components/hrxl_maxsonar_wr/sensor.py @@ -1,8 +1,6 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor, uart from esphome.const import ( - CONF_ID, STATE_CLASS_MEASUREMENT, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, @@ -16,24 +14,16 @@ HrxlMaxsonarWrComponent = hrxlmaxsonarwr_ns.class_( "HrxlMaxsonarWrComponent", sensor.Sensor, cg.Component, uart.UARTDevice ) -CONFIG_SCHEMA = ( - sensor.sensor_schema( - unit_of_measurement=UNIT_METER, - icon=ICON_ARROW_EXPAND_VERTICAL, - accuracy_decimals=3, - state_class=STATE_CLASS_MEASUREMENT, - ) - .extend( - { - cv.GenerateID(): cv.declare_id(HrxlMaxsonarWrComponent), - } - ) - .extend(uart.UART_DEVICE_SCHEMA) -) +CONFIG_SCHEMA = sensor.sensor_schema( + HrxlMaxsonarWrComponent, + unit_of_measurement=UNIT_METER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, +).extend(uart.UART_DEVICE_SCHEMA) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) await uart.register_uart_device(var, config) diff --git a/esphome/components/hte501/__init__.py b/esphome/components/hte501/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp new file mode 100644 index 0000000000..68edd07a22 --- /dev/null +++ b/esphome/components/hte501/hte501.cpp @@ -0,0 +1,90 @@ +#include "hte501.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace hte501 { + +static const char *const TAG = "hte501"; + +void HTE501Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up HTE501..."); + uint8_t address[] = {0x70, 0x29}; + this->write(address, 2, false); + uint8_t identification[9]; + this->read(identification, 9); + if (identification[8] != calc_crc8_(identification, 0, 7)) { + this->error_code_ = CRC_CHECK_FAILED; + this->mark_failed(); + return; + } + ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex(identification + 0, 7).c_str()); +} + +void HTE501Component::dump_config() { + ESP_LOGCONFIG(TAG, "HTE501:"); + LOG_I2C_DEVICE(this); + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Communication with HTE501 failed!"); + break; + case CRC_CHECK_FAILED: + ESP_LOGE(TAG, "The crc check failed"); + break; + case NONE: + default: + break; + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +float HTE501Component::get_setup_priority() const { return setup_priority::DATA; } +void HTE501Component::update() { + uint8_t address_1[] = {0x2C, 0x1B}; + this->write(address_1, 2, true); + this->set_timeout(50, [this]() { + uint8_t i2c_response[6]; + this->read(i2c_response, 6); + if (i2c_response[2] != calc_crc8_(i2c_response, 0, 1) && i2c_response[5] != calc_crc8_(i2c_response, 3, 4)) { + this->error_code_ = CRC_CHECK_FAILED; + this->status_set_warning(); + return; + } + float temperature = (float) encode_uint16(i2c_response[0], i2c_response[1]); + if (temperature > 55536) { + temperature = (temperature - 65536) / 100; + } else { + temperature = temperature / 100; + } + float humidity = ((float) encode_uint16(i2c_response[3], i2c_response[4])) / 100.0f; + + ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); + this->status_clear_warning(); + }); +} + +unsigned char HTE501Component::calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to) { + unsigned char crc_val = 0xFF; + unsigned char i = 0; + unsigned char j = 0; + for (i = from; i <= to; i++) { + int cur_val = buf[i]; + for (j = 0; j < 8; j++) { + if (((crc_val ^ cur_val) & 0x80) != 0) // If MSBs are not equal + { + crc_val = ((crc_val << 1) ^ 0x31); + } else { + crc_val = (crc_val << 1); + } + cur_val = cur_val << 1; + } + } + return crc_val; +} +} // namespace hte501 +} // namespace esphome diff --git a/esphome/components/hte501/hte501.h b/esphome/components/hte501/hte501.h new file mode 100644 index 0000000000..0d2c952e81 --- /dev/null +++ b/esphome/components/hte501/hte501.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace hte501 { + +/// This class implements support for the hte501 of temperature i2c sensors. +class HTE501Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + + float get_setup_priority() const override; + void setup() override; + void dump_config() override; + void update() override; + + protected: + unsigned char calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to); + sensor::Sensor *temperature_sensor_; + sensor::Sensor *humidity_sensor_; + + enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; +}; + +} // namespace hte501 +} // namespace esphome diff --git a/esphome/components/hte501/sensor.py b/esphome/components/hte501/sensor.py new file mode 100644 index 0000000000..8bd6160038 --- /dev/null +++ b/esphome/components/hte501/sensor.py @@ -0,0 +1,58 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_HUMIDITY, + CONF_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +CODEOWNERS = ["@Stock-M"] + +DEPENDENCIES = ["i2c"] + +hte501_ns = cg.esphome_ns.namespace("hte501") +HTE501Component = hte501_ns.class_( + "HTE501Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HTE501Component), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x40)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_HUMIDITY in config: + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index c8c0ca5369..0c3e249512 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -195,6 +195,8 @@ async def http_request_action_to_code(config, action_id, template_arg, args): for conf in config.get(CONF_ON_RESPONSE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) cg.add(var.register_response_trigger(trigger)) - await automation.build_automation(trigger, [(int, "status_code")], conf) + await automation.build_automation( + trigger, [(int, "status_code"), (cg.uint32, "duration_ms")], conf + ) return var diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 64f3f97de9..46894a9afd 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -66,6 +66,9 @@ void HttpRequestComponent::send(const std::vector } this->client_.setTimeout(this->timeout_); +#if defined(USE_ESP32) + this->client_.setConnectTimeout(this->timeout_); +#endif if (this->useragent_ != nullptr) { this->client_.setUserAgent(this->useragent_); } @@ -73,25 +76,27 @@ void HttpRequestComponent::send(const std::vector this->client_.addHeader(header.name, header.value, false, true); } + uint32_t start_time = millis(); int http_code = this->client_.sendRequest(this->method_, this->body_.c_str()); + uint32_t duration = millis() - start_time; for (auto *trigger : response_triggers) - trigger->process(http_code); + trigger->process(http_code, duration); if (http_code < 0) { - ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", this->url_.c_str(), - HTTPClient::errorToString(http_code).c_str()); + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s; Duration: %u ms", this->url_.c_str(), + HTTPClient::errorToString(http_code).c_str(), duration); this->status_set_warning(); return; } if (http_code < 200 || http_code >= 300) { - ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Code: %d", this->url_.c_str(), http_code); + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Code: %d; Duration: %u ms", this->url_.c_str(), http_code, duration); this->status_set_warning(); return; } this->status_clear_warning(); - ESP_LOGD(TAG, "HTTP Request completed; URL: %s; Code: %d", this->url_.c_str(), http_code); + ESP_LOGD(TAG, "HTTP Request completed; URL: %s; Code: %d; Duration: %u ms", this->url_.c_str(), http_code, duration); } #ifdef USE_ESP8266 @@ -120,10 +125,16 @@ void HttpRequestComponent::close() { } const char *HttpRequestComponent::get_string() { - // The static variable is here because HTTPClient::getString() returns a String on ESP32, and we need something to - // to keep a buffer alive. - static std::string str; - str = this->client_.getString().c_str(); +#if defined(ESP32) + // The static variable is here because HTTPClient::getString() returns a String on ESP32, + // and we need something to keep a buffer alive. + static String str; +#else + // However on ESP8266, HTTPClient::getString() returns a String& to a member variable. + // Leaving this the default so that any new platform either doesn't copy, or encounters a compilation error. + auto & +#endif + str = this->client_.getString(); return str.c_str(); } diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 4590163f2c..0958c07683 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -6,10 +6,12 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" + #include #include -#include #include +#include +#include #ifdef USE_ESP32 #include @@ -29,7 +31,10 @@ struct Header { const char *value; }; -class HttpRequestResponseTrigger; +class HttpRequestResponseTrigger : public Trigger { + public: + void process(int32_t status_code, uint32_t duration_ms) { this->trigger(status_code, duration_ms); } +}; class HttpRequestComponent : public Component { public: @@ -136,11 +141,6 @@ template class HttpRequestSendAction : public Action { std::vector response_triggers_; }; -class HttpRequestResponseTrigger : public Trigger { - public: - void process(int status_code) { this->trigger(status_code); } -}; - } // namespace http_request } // namespace esphome diff --git a/esphome/components/hx711/sensor.py b/esphome/components/hx711/sensor.py index cd06cc770f..88a0bb85b7 100644 --- a/esphome/components/hx711/sensor.py +++ b/esphome/components/hx711/sensor.py @@ -5,7 +5,6 @@ from esphome.components import sensor from esphome.const import ( CONF_CLK_PIN, CONF_GAIN, - CONF_ID, ICON_SCALE, STATE_CLASS_MEASUREMENT, ) @@ -24,13 +23,13 @@ GAINS = { CONFIG_SCHEMA = ( sensor.sensor_schema( + HX711Sensor, icon=ICON_SCALE, accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, ) .extend( { - cv.GenerateID(): cv.declare_id(HX711Sensor), cv.Required(CONF_DOUT_PIN): pins.gpio_input_pin_schema, cv.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_GAIN, default=128): cv.enum(GAINS, int=True), @@ -41,9 +40,8 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) dout_pin = await cg.gpio_pin_expression(config[CONF_DOUT_PIN]) cg.add(var.set_dout_pin(dout_pin)) diff --git a/esphome/components/hydreon_rgxx/__init__.py b/esphome/components/hydreon_rgxx/__init__.py new file mode 100644 index 0000000000..5fe050edf2 --- /dev/null +++ b/esphome/components/hydreon_rgxx/__init__.py @@ -0,0 +1,11 @@ +import esphome.codegen as cg +from esphome.components import uart + +CODEOWNERS = ["@functionpointer"] +DEPENDENCIES = ["uart"] + +hydreon_rgxx_ns = cg.esphome_ns.namespace("hydreon_rgxx") +RGModel = hydreon_rgxx_ns.enum("RGModel") +HydreonRGxxComponent = hydreon_rgxx_ns.class_( + "HydreonRGxxComponent", cg.PollingComponent, uart.UARTDevice +) diff --git a/esphome/components/hydreon_rgxx/binary_sensor.py b/esphome/components/hydreon_rgxx/binary_sensor.py new file mode 100644 index 0000000000..776be8a5d8 --- /dev/null +++ b/esphome/components/hydreon_rgxx/binary_sensor.py @@ -0,0 +1,53 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_COLD, + DEVICE_CLASS_PROBLEM, +) + +from . import hydreon_rgxx_ns, HydreonRGxxComponent + +CONF_HYDREON_RGXX_ID = "hydreon_rgxx_id" +CONF_TOO_COLD = "too_cold" +CONF_LENS_BAD = "lens_bad" +CONF_EM_SAT = "em_sat" + +HydreonRGxxBinarySensor = hydreon_rgxx_ns.class_( + "HydreonRGxxBinaryComponent", cg.Component +) + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(HydreonRGxxBinarySensor), + cv.GenerateID(CONF_HYDREON_RGXX_ID): cv.use_id(HydreonRGxxComponent), + cv.Optional(CONF_TOO_COLD): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_COLD + ), + cv.Optional(CONF_LENS_BAD): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + ), + cv.Optional(CONF_EM_SAT): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + ), + } +) + + +async def to_code(config): + main_sensor = await cg.get_variable(config[CONF_HYDREON_RGXX_ID]) + bin_component = cg.new_Pvariable(config[CONF_ID], main_sensor) + await cg.register_component(bin_component, config) + + mapping = { + CONF_TOO_COLD: main_sensor.set_too_cold_sensor, + CONF_LENS_BAD: main_sensor.set_lens_bad_sensor, + CONF_EM_SAT: main_sensor.set_em_sat_sensor, + } + + for key, value in mapping.items(): + if key in config: + sensor = await binary_sensor.new_binary_sensor(config[key]) + cg.add(value(sensor)) diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp new file mode 100644 index 0000000000..da4345e136 --- /dev/null +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp @@ -0,0 +1,252 @@ +#include "hydreon_rgxx.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace hydreon_rgxx { + +static const char *const TAG = "hydreon_rgxx.sensor"; +static const int MAX_DATA_LENGTH_BYTES = 80; +static const uint8_t ASCII_LF = 0x0A; +#define HYDREON_RGXX_COMMA , +static const char *const PROTOCOL_NAMES[] = {HYDREON_RGXX_PROTOCOL_LIST(, HYDREON_RGXX_COMMA)}; +static const char *const IGNORE_STRINGS[] = {HYDREON_RGXX_IGNORE_LIST(, HYDREON_RGXX_COMMA)}; + +void HydreonRGxxComponent::dump_config() { + this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8); + ESP_LOGCONFIG(TAG, "hydreon_rgxx:"); + if (this->is_failed()) { + ESP_LOGE(TAG, "Connection with hydreon_rgxx failed!"); + } + LOG_UPDATE_INTERVAL(this); + + int i = 0; +#define HYDREON_RGXX_LOG_SENSOR(s) \ + if (this->sensors_[i++] != nullptr) { \ + LOG_SENSOR(" ", #s, this->sensors_[i - 1]); \ + } + HYDREON_RGXX_PROTOCOL_LIST(HYDREON_RGXX_LOG_SENSOR, ); +} + +void HydreonRGxxComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up hydreon_rgxx..."); + while (this->available() != 0) { + this->read(); + } + this->schedule_reboot_(); +} + +int HydreonRGxxComponent::num_sensors_missing_() { + if (this->sensors_received_ == -1) { + return -1; + } + int ret = NUM_SENSORS; + for (int i = 0; i < NUM_SENSORS; i++) { + if (this->sensors_[i] == nullptr) { + ret -= 1; + continue; + } + if ((this->sensors_received_ >> i & 1) != 0) { + ret -= 1; + } + } + return ret; +} + +void HydreonRGxxComponent::update() { + if (this->boot_count_ > 0) { + if (this->num_sensors_missing_() > 0) { + for (int i = 0; i < NUM_SENSORS; i++) { + if (this->sensors_[i] == nullptr) { + continue; + } + if ((this->sensors_received_ >> i & 1) == 0) { + ESP_LOGW(TAG, "Missing %s", PROTOCOL_NAMES[i]); + } + } + + this->no_response_count_++; + ESP_LOGE(TAG, "missing %d sensors; %d times in a row", this->num_sensors_missing_(), this->no_response_count_); + if (this->no_response_count_ > 15) { + ESP_LOGE(TAG, "asking sensor to reboot"); + for (auto &sensor : this->sensors_) { + if (sensor != nullptr) { + sensor->publish_state(NAN); + } + } + this->schedule_reboot_(); + return; + } + } else { + this->no_response_count_ = 0; + } + this->write_str("R\n"); +#ifdef USE_BINARY_SENSOR + if (this->too_cold_sensor_ != nullptr) { + this->too_cold_sensor_->publish_state(this->too_cold_); + } + if (this->lens_bad_sensor_ != nullptr) { + this->lens_bad_sensor_->publish_state(this->lens_bad_); + } + if (this->em_sat_sensor_ != nullptr) { + this->em_sat_sensor_->publish_state(this->em_sat_); + } +#endif + this->too_cold_ = false; + this->lens_bad_ = false; + this->em_sat_ = false; + this->sensors_received_ = 0; + } +} + +void HydreonRGxxComponent::loop() { + uint8_t data; + while (this->available() > 0) { + if (this->read_byte(&data)) { + buffer_ += (char) data; + if (this->buffer_.back() == static_cast(ASCII_LF) || this->buffer_.length() >= MAX_DATA_LENGTH_BYTES) { + // complete line received + this->process_line_(); + this->buffer_.clear(); + } + } + } +} + +/** + * Communication with the sensor is asynchronous. + * We send requests and let esphome continue doing its thing. + * Once we have received a complete line, we process it. + * + * Catching communication failures is done in two layers: + * + * 1. We check if all requested data has been received + * before we send out the next request. If data keeps + * missing, we escalate. + * 2. Request the sensor to reboot. We retry based on + * a timeout. If the sensor does not respond after + * several boot attempts, we give up. + */ +void HydreonRGxxComponent::schedule_reboot_() { + this->boot_count_ = 0; + this->set_interval("reboot", 5000, [this]() { + if (this->boot_count_ < 0) { + ESP_LOGW(TAG, "hydreon_rgxx failed to boot %d times", -this->boot_count_); + } + this->boot_count_--; + this->write_str("K\n"); + if (this->boot_count_ < -5) { + ESP_LOGE(TAG, "hydreon_rgxx can't boot, giving up"); + for (auto &sensor : this->sensors_) { + if (sensor != nullptr) { + sensor->publish_state(NAN); + } + } + this->mark_failed(); + } + }); +} + +bool HydreonRGxxComponent::buffer_starts_with_(const std::string &prefix) { + return this->buffer_starts_with_(prefix.c_str()); +} + +bool HydreonRGxxComponent::buffer_starts_with_(const char *prefix) { return buffer_.rfind(prefix, 0) == 0; } + +void HydreonRGxxComponent::process_line_() { + ESP_LOGV(TAG, "Read from serial: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); + + if (buffer_[0] == ';') { + ESP_LOGI(TAG, "Comment: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); + return; + } + std::string::size_type newlineposn = this->buffer_.find('\n'); + if (newlineposn <= 1) { + // allow both \r\n and \n + ESP_LOGD(TAG, "Received empty line"); + return; + } + if (newlineposn <= 2) { + // single character lines, such as acknowledgements + ESP_LOGD(TAG, "Received ack: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); + return; + } + if (this->buffer_.find("LensBad") != std::string::npos) { + ESP_LOGW(TAG, "Received LensBad!"); + this->lens_bad_ = true; + } + if (this->buffer_.find("EmSat") != std::string::npos) { + ESP_LOGW(TAG, "Received EmSat!"); + this->em_sat_ = true; + } + if (this->buffer_starts_with_("PwrDays")) { + if (this->boot_count_ <= 0) { + this->boot_count_ = 1; + } else { + this->boot_count_++; + } + this->cancel_interval("reboot"); + this->no_response_count_ = 0; + ESP_LOGI(TAG, "Boot detected: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); + this->write_str("P\nH\nM\n"); // set sensor to polling mode, high res mode, metric mode + return; + } + if (this->buffer_starts_with_("SW")) { + std::string::size_type majend = this->buffer_.find('.'); + std::string::size_type endversion = this->buffer_.find(' ', 3); + if (majend == std::string::npos || endversion == std::string::npos || majend > endversion) { + ESP_LOGW(TAG, "invalid version string: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); + } + int major = strtol(this->buffer_.substr(3, majend - 3).c_str(), nullptr, 10); + int minor = strtol(this->buffer_.substr(majend + 1, endversion - (majend + 1)).c_str(), nullptr, 10); + + if (major > 10 || minor >= 1000 || minor < 0 || major < 0) { + ESP_LOGW(TAG, "invalid version: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); + } + this->sw_version_ = major * 1000 + minor; + ESP_LOGI(TAG, "detected sw version %i", this->sw_version_); + return; + } + bool is_data_line = false; + for (int i = 0; i < NUM_SENSORS; i++) { + if (this->sensors_[i] != nullptr && this->buffer_starts_with_(PROTOCOL_NAMES[i])) { + is_data_line = true; + break; + } + } + if (is_data_line) { + std::string::size_type tc = this->buffer_.find("TooCold"); + this->too_cold_ |= tc != std::string::npos; + if (this->too_cold_) { + ESP_LOGD(TAG, "Received TooCold"); + } + for (int i = 0; i < NUM_SENSORS; i++) { + if (this->sensors_[i] == nullptr) { + continue; + } + std::string::size_type n = this->buffer_.find(PROTOCOL_NAMES[i]); + if (n == std::string::npos) { + continue; + } + float data = strtof(this->buffer_.substr(n + strlen(PROTOCOL_NAMES[i])).c_str(), nullptr); + this->sensors_[i]->publish_state(data); + ESP_LOGD(TAG, "Received %s: %f", PROTOCOL_NAMES[i], this->sensors_[i]->get_raw_state()); + this->sensors_received_ |= (1 << i); + } + if (this->request_temperature_ && this->num_sensors_missing_() == 1) { + this->write_str("T\n"); + } + } else { + for (const auto *ignore : IGNORE_STRINGS) { + if (this->buffer_starts_with_(ignore)) { + ESP_LOGI(TAG, "Ignoring %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); + return; + } + } + ESP_LOGI(TAG, "Got unknown line: %s", this->buffer_.c_str()); + } +} + +float HydreonRGxxComponent::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace hydreon_rgxx +} // namespace esphome diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.h b/esphome/components/hydreon_rgxx/hydreon_rgxx.h new file mode 100644 index 0000000000..34b9bd8d5e --- /dev/null +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.h @@ -0,0 +1,86 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/components/sensor/sensor.h" +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace hydreon_rgxx { + +enum RGModel { + RG9 = 1, + RG15 = 2, +}; + +#ifdef HYDREON_RGXX_NUM_SENSORS +static const uint8_t NUM_SENSORS = HYDREON_RGXX_NUM_SENSORS; +#else +static const uint8_t NUM_SENSORS = 1; +#endif + +#ifndef HYDREON_RGXX_PROTOCOL_LIST +#define HYDREON_RGXX_PROTOCOL_LIST(F, SEP) F("") +#endif + +#define HYDREON_RGXX_IGNORE_LIST(F, SEP) F("Emitters") SEP F("Event") SEP F("Reset") + +class HydreonRGxxComponent : public PollingComponent, public uart::UARTDevice { + public: + void set_sensor(sensor::Sensor *sensor, int index) { this->sensors_[index] = sensor; } +#ifdef USE_BINARY_SENSOR + void set_too_cold_sensor(binary_sensor::BinarySensor *sensor) { this->too_cold_sensor_ = sensor; } + void set_lens_bad_sensor(binary_sensor::BinarySensor *sensor) { this->lens_bad_sensor_ = sensor; } + void set_em_sat_sensor(binary_sensor::BinarySensor *sensor) { this->em_sat_sensor_ = sensor; } +#endif + void set_model(RGModel model) { model_ = model; } + void set_request_temperature(bool b) { request_temperature_ = b; } + + /// Schedule data readings. + void update() override; + /// Read data once available + void loop() override; + /// Setup the sensor and test for a connection. + void setup() override; + void dump_config() override; + + float get_setup_priority() const override; + + protected: + void process_line_(); + void schedule_reboot_(); + bool buffer_starts_with_(const std::string &prefix); + bool buffer_starts_with_(const char *prefix); + int num_sensors_missing_(); + + sensor::Sensor *sensors_[NUM_SENSORS] = {nullptr}; +#ifdef USE_BINARY_SENSOR + binary_sensor::BinarySensor *too_cold_sensor_{nullptr}; + binary_sensor::BinarySensor *lens_bad_sensor_{nullptr}; + binary_sensor::BinarySensor *em_sat_sensor_{nullptr}; +#endif + + int16_t boot_count_ = 0; + int16_t no_response_count_ = 0; + std::string buffer_; + RGModel model_ = RG9; + int sw_version_ = 0; + bool too_cold_ = false; + bool lens_bad_ = false; + bool em_sat_ = false; + bool request_temperature_ = false; + + // bit field showing which sensors we have received data for + int sensors_received_ = -1; +}; + +class HydreonRGxxBinaryComponent : public Component { + public: + HydreonRGxxBinaryComponent(HydreonRGxxComponent *parent) {} +}; + +} // namespace hydreon_rgxx +} // namespace esphome diff --git a/esphome/components/hydreon_rgxx/sensor.py b/esphome/components/hydreon_rgxx/sensor.py new file mode 100644 index 0000000000..c2dbbd6737 --- /dev/null +++ b/esphome/components/hydreon_rgxx/sensor.py @@ -0,0 +1,134 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart, sensor +from esphome.const import ( + CONF_ID, + CONF_MODEL, + CONF_MOISTURE, + CONF_TEMPERATURE, + DEVICE_CLASS_PRECIPITATION_INTENSITY, + DEVICE_CLASS_PRECIPITATION, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_CELSIUS, + ICON_THERMOMETER, +) + +from . import RGModel, HydreonRGxxComponent + +UNIT_INTENSITY = "intensity" +UNIT_MILLIMETERS = "mm" +UNIT_MILLIMETERS_PER_HOUR = "mm/h" + +CONF_ACC = "acc" +CONF_EVENT_ACC = "event_acc" +CONF_TOTAL_ACC = "total_acc" +CONF_R_INT = "r_int" + +RG_MODELS = { + "RG_9": RGModel.RG9, + "RG_15": RGModel.RG15, + # https://rainsensors.com/wp-content/uploads/sites/3/2020/07/rg-15_instructions_sw_1.000.pdf + # https://rainsensors.com/wp-content/uploads/sites/3/2021/03/2020.08.25-rg-9_instructions.pdf + # https://rainsensors.com/wp-content/uploads/sites/3/2021/03/2021.03.11-rg-9_instructions.pdf +} +SUPPORTED_SENSORS = { + CONF_ACC: ["RG_15"], + CONF_EVENT_ACC: ["RG_15"], + CONF_TOTAL_ACC: ["RG_15"], + CONF_R_INT: ["RG_15"], + CONF_MOISTURE: ["RG_9"], + CONF_TEMPERATURE: ["RG_9"], +} +PROTOCOL_NAMES = { + CONF_MOISTURE: "R", + CONF_ACC: "Acc", + CONF_R_INT: "RInt", + CONF_EVENT_ACC: "EventAcc", + CONF_TOTAL_ACC: "TotalAcc", + CONF_TEMPERATURE: "t", +} + + +def _validate(config): + for conf, models in SUPPORTED_SENSORS.items(): + if conf in config: + if config[CONF_MODEL] not in models: + raise cv.Invalid( + f"{conf} is only available on {' and '.join(models)}, not {config[CONF_MODEL]}" + ) + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HydreonRGxxComponent), + cv.Required(CONF_MODEL): cv.enum( + RG_MODELS, + upper=True, + space="_", + ), + cv.Optional(CONF_ACC): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIMETERS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PRECIPITATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_EVENT_ACC): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIMETERS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PRECIPITATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TOTAL_ACC): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIMETERS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PRECIPITATION, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_R_INT): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIMETERS_PER_HOUR, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PRECIPITATION_INTENSITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_MOISTURE): sensor.sensor_schema( + unit_of_measurement=UNIT_INTENSITY, + accuracy_decimals=0, + device_class=DEVICE_CLASS_PRECIPITATION_INTENSITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=0, + icon=ICON_THERMOMETER, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA), + _validate, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + cg.add_define( + "HYDREON_RGXX_PROTOCOL_LIST(F, sep)", + cg.RawExpression( + " sep ".join([f'F("{name} ")' for name in PROTOCOL_NAMES.values()]) + ), + ) + cg.add_define("HYDREON_RGXX_NUM_SENSORS", len(PROTOCOL_NAMES)) + + for i, conf in enumerate(PROTOCOL_NAMES): + if conf in config: + sens = await sensor.new_sensor(config[conf]) + cg.add(var.set_sensor(sens, i)) + + cg.add(var.set_request_temperature(CONF_TEMPERATURE in config)) diff --git a/esphome/components/hyt271/__init__.py b/esphome/components/hyt271/__init__.py new file mode 100644 index 0000000000..2e88d4f366 --- /dev/null +++ b/esphome/components/hyt271/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@Philippe12"] diff --git a/esphome/components/hyt271/hyt271.cpp b/esphome/components/hyt271/hyt271.cpp new file mode 100644 index 0000000000..94558fff04 --- /dev/null +++ b/esphome/components/hyt271/hyt271.cpp @@ -0,0 +1,52 @@ +#include "hyt271.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace hyt271 { + +static const char *const TAG = "hyt271"; + +static const uint8_t HYT271_ADDRESS = 0x28; + +void HYT271Component::dump_config() { + ESP_LOGCONFIG(TAG, "HYT271:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); +} +void HYT271Component::update() { + uint8_t raw_data[4]; + + if (this->write(&raw_data[0], 0) != i2c::ERROR_OK) { + this->status_set_warning(); + ESP_LOGE(TAG, "Communication with HYT271 failed! => Ask new values"); + return; + } + this->set_timeout("wait_convert", 50, [this]() { + uint8_t raw_data[4]; + if (this->read(raw_data, 4) != i2c::ERROR_OK) { + this->status_set_warning(); + ESP_LOGE(TAG, "Communication with HYT271 failed! => Read values"); + return; + } + uint16_t raw_temperature = ((raw_data[2] << 8) | raw_data[3]) >> 2; + uint16_t raw_humidity = ((raw_data[0] & 0x3F) << 8) | raw_data[1]; + + float temperature = ((float(raw_temperature)) * (165.0f / 16383.0f)) - 40.0f; + float humidity = (float(raw_humidity)) * (100.0f / 16383.0f); + + ESP_LOGD(TAG, "Got Temperature=%.1f°C Humidity=%.1f%%", temperature, humidity); + + if (this->temperature_ != nullptr) + this->temperature_->publish_state(temperature); + if (this->humidity_ != nullptr) + this->humidity_->publish_state(humidity); + this->status_clear_warning(); + }); +} +float HYT271Component::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace hyt271 +} // namespace esphome diff --git a/esphome/components/hyt271/hyt271.h b/esphome/components/hyt271/hyt271.h new file mode 100644 index 0000000000..64f32a651c --- /dev/null +++ b/esphome/components/hyt271/hyt271.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace hyt271 { + +class HYT271Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + + void dump_config() override; + /// Update the sensor values (temperature+humidity). + void update() override; + + float get_setup_priority() const override; + + protected: + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; +}; + +} // namespace hyt271 +} // namespace esphome diff --git a/esphome/components/hyt271/sensor.py b/esphome/components/hyt271/sensor.py new file mode 100644 index 0000000000..2ec2836461 --- /dev/null +++ b/esphome/components/hyt271/sensor.py @@ -0,0 +1,56 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +DEPENDENCIES = ["i2c"] + +hyt271_ns = cg.esphome_ns.namespace("hyt271") +HYT271Component = hyt271_ns.class_( + "HYT271Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HYT271Component), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x28)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + + if CONF_HUMIDITY in config: + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 46f0abacc6..a04e63e789 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv +import esphome.final_validate as fv from esphome import pins from esphome.const import ( CONF_FREQUENCY, @@ -110,3 +111,27 @@ async def register_i2c_device(var, config): parent = await cg.get_variable(config[CONF_I2C_ID]) cg.add(var.set_i2c_bus(parent)) cg.add(var.set_i2c_address(config[CONF_ADDRESS])) + + +def final_validate_device_schema( + name: str, *, min_frequency: cv.frequency = None, max_frequency: cv.frequency = None +): + hub_schema = {} + if min_frequency is not None: + hub_schema[cv.Required(CONF_FREQUENCY)] = cv.Range( + min=cv.frequency(min_frequency), + min_included=True, + msg=f"Component {name} requires a minimum frequency of {min_frequency} for the I2C bus", + ) + + if max_frequency is not None: + hub_schema[cv.Required(CONF_FREQUENCY)] = cv.Range( + max=cv.frequency(max_frequency), + max_included=True, + msg=f"Component {name} cannot be used with a frequency of over {max_frequency} for the I2C bus", + ) + + return cv.Schema( + {cv.Required(CONF_I2C_ID): fv.id_declaration_match_schema(hub_schema)}, + extra=cv.ALLOW_EXTRA, + ) diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 82ab7bd09a..2b2190d28b 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -7,6 +7,48 @@ namespace i2c { static const char *const TAG = "i2c"; +ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop) { + ErrorCode err = this->write(&a_register, 1, stop); + if (err != ERROR_OK) + return err; + return bus_->read(address_, data, len); +} + +ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop) { + a_register = convert_big_endian(a_register); + ErrorCode const err = this->write(reinterpret_cast(&a_register), 2, stop); + if (err != ERROR_OK) + return err; + return bus_->read(address_, data, len); +} + +ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) { + WriteBuffer buffers[2]; + buffers[0].data = &a_register; + buffers[0].len = 1; + buffers[1].data = data; + buffers[1].len = len; + return bus_->writev(address_, buffers, 2, stop); +} + +ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop) { + a_register = convert_big_endian(a_register); + WriteBuffer buffers[2]; + buffers[0].data = reinterpret_cast(&a_register); + buffers[0].len = 2; + buffers[1].data = data; + buffers[1].len = len; + return bus_->writev(address_, buffers, 2, stop); +} + +bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { + if (read_register(a_register, reinterpret_cast(data), len * 2) != ERROR_OK) + return false; + for (size_t i = 0; i < len; i++) + data[i] = i2ctohs(data[i]); + return true; +} + bool I2CDevice::write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) { // we have to copy in order to be able to change byte order std::unique_ptr temp{new uint16_t[len]}; @@ -36,5 +78,26 @@ uint8_t I2CRegister::get() const { return value; } +I2CRegister16 &I2CRegister16::operator=(uint8_t value) { + this->parent_->write_register16(this->register_, &value, 1); + return *this; +} +I2CRegister16 &I2CRegister16::operator&=(uint8_t value) { + value &= get(); + this->parent_->write_register16(this->register_, &value, 1); + return *this; +} +I2CRegister16 &I2CRegister16::operator|=(uint8_t value) { + value |= get(); + this->parent_->write_register16(this->register_, &value, 1); + return *this; +} + +uint8_t I2CRegister16::get() const { + uint8_t value = 0x00; + this->parent_->read_register16(this->register_, &value, 1); + return value; +} + } // namespace i2c } // namespace esphome diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index 50a0b3ae50..eb5d463b65 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -31,6 +31,25 @@ class I2CRegister { uint8_t register_; }; +class I2CRegister16 { + public: + I2CRegister16 &operator=(uint8_t value); + I2CRegister16 &operator&=(uint8_t value); + I2CRegister16 &operator|=(uint8_t value); + + explicit operator uint8_t() const { return get(); } + + uint8_t get() const; + + protected: + friend class I2CDevice; + + I2CRegister16(I2CDevice *parent, uint16_t a_register) : parent_(parent), register_(a_register) {} + + I2CDevice *parent_; + uint16_t register_; +}; + // like ntohs/htons but without including networking headers. // ("i2c" byte order is big-endian) inline uint16_t i2ctohs(uint16_t i2cshort) { return convert_big_endian(i2cshort); } @@ -44,24 +63,15 @@ class I2CDevice { void set_i2c_bus(I2CBus *bus) { bus_ = bus; } I2CRegister reg(uint8_t a_register) { return {this, a_register}; } + I2CRegister16 reg16(uint16_t a_register) { return {this, a_register}; } ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); } - ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len) { - ErrorCode err = this->write(&a_register, 1); - if (err != ERROR_OK) - return err; - return this->read(data, len); - } + ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true); + ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop = true); - ErrorCode write(const uint8_t *data, uint8_t len) { return bus_->write(address_, data, len); } - ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len) { - WriteBuffer buffers[2]; - buffers[0].data = &a_register; - buffers[0].len = 1; - buffers[1].data = data; - buffers[1].len = len; - return bus_->writev(address_, buffers, 2); - } + ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); } + ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true); + ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop = true); // Compat APIs @@ -85,15 +95,11 @@ class I2CDevice { return res; } - bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { - if (read_register(a_register, reinterpret_cast(data), len * 2) != ERROR_OK) - return false; - for (size_t i = 0; i < len; i++) - data[i] = i2ctohs(data[i]); - return true; - } + bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len); - bool read_byte(uint8_t a_register, uint8_t *data) { return read_register(a_register, data, 1) == ERROR_OK; } + bool read_byte(uint8_t a_register, uint8_t *data, bool stop = true) { + return read_register(a_register, data, 1, stop) == ERROR_OK; + } optional read_byte(uint8_t a_register) { uint8_t data; @@ -104,8 +110,8 @@ class I2CDevice { bool read_byte_16(uint8_t a_register, uint16_t *data) { return read_bytes_16(a_register, data, 1); } - bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len) { - return write_register(a_register, data, len) == ERROR_OK; + bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len, bool stop = true) { + return write_register(a_register, data, len, stop) == ERROR_OK; } bool write_bytes(uint8_t a_register, const std::vector &data) { @@ -118,7 +124,9 @@ class I2CDevice { bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len); - bool write_byte(uint8_t a_register, uint8_t data) { return write_bytes(a_register, &data, 1); } + bool write_byte(uint8_t a_register, uint8_t data, bool stop = true) { + return write_bytes(a_register, &data, 1, stop); + } bool write_byte_16(uint8_t a_register, uint16_t data) { return write_bytes_16(a_register, &data, 1); } diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index 71f6b1d15b..2633a7adf6 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -15,6 +15,7 @@ enum ErrorCode { ERROR_NOT_INITIALIZED = 4, ERROR_TOO_LARGE = 5, ERROR_UNKNOWN = 6, + ERROR_CRC = 7, }; struct ReadBuffer { @@ -36,12 +37,18 @@ class I2CBus { } virtual ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) = 0; virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len) { + return write(address, buffer, len, true); + } + virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len, bool stop) { WriteBuffer buf; buf.data = buffer; buf.len = len; - return writev(address, &buf, 1); + return writev(address, &buf, 1, stop); } - virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) = 0; + virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) { + return writev(address, buffers, cnt, true); + } + virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) = 0; protected: void i2c_scan_() { diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 693b869bf7..d80ab1fd1d 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -3,6 +3,7 @@ #include "i2c_bus_arduino.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/application.h" #include #include @@ -14,7 +15,7 @@ static const char *const TAG = "i2c.arduino"; void ArduinoI2CBus::setup() { recover_(); -#ifdef USE_ESP32 +#if defined(USE_ESP32) static uint8_t next_bus_num = 0; if (next_bus_num == 0) { wire_ = &Wire; @@ -22,11 +23,25 @@ void ArduinoI2CBus::setup() { wire_ = new TwoWire(next_bus_num); // NOLINT(cppcoreguidelines-owning-memory) } next_bus_num++; -#else +#elif defined(USE_ESP8266) wire_ = &Wire; // NOLINT(cppcoreguidelines-prefer-member-initializer) +#elif defined(USE_RP2040) + static bool first = true; + if (first) { + wire_ = &Wire; + first = false; + } else { + wire_ = &Wire1; // NOLINT(cppcoreguidelines-owning-memory) + } #endif +#ifdef USE_RP2040 + wire_->setSDA(this->sda_pin_); + wire_->setSCL(this->scl_pin_); + wire_->begin(); +#else wire_->begin(static_cast(sda_pin_), static_cast(scl_pin_)); +#endif wire_->setClock(frequency_); initialized_ = true; if (this->scan_) { @@ -104,7 +119,7 @@ ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) return ERROR_OK; } -ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt) { +ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) { // logging is only enabled with vv level, if warnings are shown the caller // should log them if (!initialized_) { @@ -139,19 +154,26 @@ ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cn return ERROR_UNKNOWN; } } - uint8_t status = wire_->endTransmission(true); - if (status == 0) { - return ERROR_OK; - } else if (status == 1) { - // transmit buffer not large enough - ESP_LOGVV(TAG, "TX failed: buffer not large enough"); - return ERROR_UNKNOWN; - } else if (status == 2 || status == 3) { - ESP_LOGVV(TAG, "TX failed: not acknowledged"); - return ERROR_NOT_ACKNOWLEDGED; + uint8_t status = wire_->endTransmission(stop); + switch (status) { + case 0: + return ERROR_OK; + case 1: + // transmit buffer not large enough + ESP_LOGVV(TAG, "TX failed: buffer not large enough"); + return ERROR_UNKNOWN; + case 2: + case 3: + ESP_LOGVV(TAG, "TX failed: not acknowledged"); + return ERROR_NOT_ACKNOWLEDGED; + case 5: + ESP_LOGVV(TAG, "TX failed: timeout"); + return ERROR_UNKNOWN; + case 4: + default: + ESP_LOGVV(TAG, "TX failed: unknown error %u", status); + return ERROR_UNKNOWN; } - ESP_LOGVV(TAG, "TX failed: unknown error %u", status); - return ERROR_UNKNOWN; } /// Perform I2C bus recovery, see: @@ -206,10 +228,14 @@ void ArduinoI2CBus::recover_() { // When SCL is kept LOW at this point, we might be looking at a device // that applies clock stretching. Wait for the release of the SCL line, // but not forever. There is no specification for the maximum allowed - // time. We'll stick to 500ms here. - auto wait = 20; + // time. We yield and reset the WDT, so as to avoid triggering reset. + // No point in trying to recover the bus by forcing a uC reset. Bus + // should recover in a few ms or less else not likely to recovery at + // all. + auto wait = 250; while (wait-- && digitalRead(scl_pin_) == LOW) { // NOLINT - delay(25); + App.feed_wdt(); + delayMicroseconds(half_period_usec * 2); } if (digitalRead(scl_pin_) == LOW) { // NOLINT ESP_LOGE(TAG, "Recovery failed: SCL is held LOW during clock pulse cycle"); @@ -224,7 +250,7 @@ void ArduinoI2CBus::recover_() { digitalWrite(sda_pin_, LOW); // NOLINT // By now, any stuck device ought to have sent all remaining bits of its - // transation, meaning that it should have freed up the SDA line, resulting + // transaction, meaning that it should have freed up the SDA line, resulting // in SDA being pulled up. if (digitalRead(sda_pin_) == LOW) { // NOLINT ESP_LOGE(TAG, "Recovery failed: SDA is held LOW after clock pulse cycle"); diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index f4151e4f37..7298c3a1c9 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -20,7 +20,7 @@ class ArduinoI2CBus : public I2CBus, public Component { void setup() override; void dump_config() override; ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) override; - ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) override; + ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) override; float get_setup_priority() const override { return setup_priority::BUS; } void set_scan(bool scan) { scan_ = scan; } diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index 606583fd7c..51688322f6 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -4,7 +4,9 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/application.h" #include +#include namespace esphome { namespace i2c { @@ -47,7 +49,7 @@ void IDFI2CBus::dump_config() { ESP_LOGCONFIG(TAG, "I2C Bus:"); ESP_LOGCONFIG(TAG, " SDA Pin: GPIO%u", this->sda_pin_); ESP_LOGCONFIG(TAG, " SCL Pin: GPIO%u", this->scl_pin_); - ESP_LOGCONFIG(TAG, " Frequency: %u Hz", this->frequency_); + ESP_LOGCONFIG(TAG, " Frequency: %" PRIu32 " Hz", this->frequency_); switch (this->recovery_result_) { case RECOVERY_COMPLETED: ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered"); @@ -142,7 +144,7 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { return ERROR_OK; } -ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt) { +ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) { // logging is only enabled with vv level, if warnings are shown the caller // should log them if (!initialized_) { @@ -272,10 +274,14 @@ void IDFI2CBus::recover_() { // When SCL is kept LOW at this point, we might be looking at a device // that applies clock stretching. Wait for the release of the SCL line, // but not forever. There is no specification for the maximum allowed - // time. We'll stick to 500ms here. - auto wait = 20; + // time. We yield and reset the WDT, so as to avoid triggering reset. + // No point in trying to recover the bus by forcing a uC reset. Bus + // should recover in a few ms or less else not likely to recovery at + // all. + auto wait = 250; while (wait-- && gpio_get_level(scl_pin) == 0) { - delay(25); + App.feed_wdt(); + delayMicroseconds(half_period_usec * 2); } if (gpio_get_level(scl_pin) == 0) { ESP_LOGE(TAG, "Recovery failed: SCL is held LOW during clock pulse cycle"); @@ -285,7 +291,7 @@ void IDFI2CBus::recover_() { } // By now, any stuck device ought to have sent all remaining bits of its - // transation, meaning that it should have freed up the SDA line, resulting + // transaction, meaning that it should have freed up the SDA line, resulting // in SDA being pulled up. if (gpio_get_level(sda_pin) == 0) { ESP_LOGE(TAG, "Recovery failed: SDA is held LOW after clock pulse cycle"); diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index d4b0626467..c80ea8c99d 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -20,7 +20,7 @@ class IDFI2CBus : public I2CBus, public Component { void setup() override; void dump_config() override; ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) override; - ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) override; + ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) override; float get_setup_priority() const override { return setup_priority::BUS; } void set_scan(bool scan) { scan_ = scan; } diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py new file mode 100644 index 0000000000..d72e13630f --- /dev/null +++ b/esphome/components/i2s_audio/__init__.py @@ -0,0 +1,75 @@ +import esphome.config_validation as cv +import esphome.final_validate as fv +import esphome.codegen as cg + +from esphome import pins +from esphome.const import CONF_ID +from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + VARIANT_ESP32C3, +) + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["esp32"] +MULTI_CONF = True + +CONF_I2S_DOUT_PIN = "i2s_dout_pin" +CONF_I2S_DIN_PIN = "i2s_din_pin" +CONF_I2S_MCLK_PIN = "i2s_mclk_pin" +CONF_I2S_BCLK_PIN = "i2s_bclk_pin" +CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin" + +CONF_I2S_AUDIO = "i2s_audio" +CONF_I2S_AUDIO_ID = "i2s_audio_id" + +i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio") +I2SAudioComponent = i2s_audio_ns.class_("I2SAudioComponent", cg.Component) +I2SAudioIn = i2s_audio_ns.class_("I2SAudioIn", cg.Parented.template(I2SAudioComponent)) +I2SAudioOut = i2s_audio_ns.class_( + "I2SAudioOut", cg.Parented.template(I2SAudioComponent) +) + +# https://github.com/espressif/esp-idf/blob/master/components/soc/{variant}/include/soc/soc_caps.h +I2S_PORTS = { + VARIANT_ESP32: 2, + VARIANT_ESP32S2: 1, + VARIANT_ESP32S3: 2, + VARIANT_ESP32C3: 1, +} + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(I2SAudioComponent), + cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_I2S_MCLK_PIN): pins.internal_gpio_output_pin_number, + } +) + + +def _final_validate(_): + i2s_audio_configs = fv.full_config.get()[CONF_I2S_AUDIO] + variant = get_esp32_variant() + if variant not in I2S_PORTS: + raise cv.Invalid(f"Unsupported variant {variant}") + if len(i2s_audio_configs) > I2S_PORTS[variant]: + raise cv.Invalid( + f"Only {I2S_PORTS[variant]} I2S audio ports are supported on {variant}" + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) + if CONF_I2S_BCLK_PIN in config: + cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN])) + if CONF_I2S_MCLK_PIN in config: + cg.add(var.set_mclk_pin(config[CONF_I2S_MCLK_PIN])) diff --git a/esphome/components/i2s_audio/i2s_audio.cpp b/esphome/components/i2s_audio/i2s_audio.cpp new file mode 100644 index 0000000000..c1a608c064 --- /dev/null +++ b/esphome/components/i2s_audio/i2s_audio.cpp @@ -0,0 +1,30 @@ +#include "i2s_audio.h" + +#ifdef USE_ESP32 + +#include "esphome/core/log.h" + +namespace esphome { +namespace i2s_audio { + +static const char *const TAG = "i2s_audio"; + +void I2SAudioComponent::setup() { + static i2s_port_t next_port_num = I2S_NUM_0; + + if (next_port_num >= I2S_NUM_MAX) { + ESP_LOGE(TAG, "Too many I2S Audio components!"); + this->mark_failed(); + return; + } + + this->port_ = next_port_num; + next_port_num = (i2s_port_t) (next_port_num + 1); + + ESP_LOGCONFIG(TAG, "Setting up I2S Audio..."); +} + +} // namespace i2s_audio +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/i2s_audio/i2s_audio.h b/esphome/components/i2s_audio/i2s_audio.h new file mode 100644 index 0000000000..d8d4a23dde --- /dev/null +++ b/esphome/components/i2s_audio/i2s_audio.h @@ -0,0 +1,57 @@ +#pragma once + +#ifdef USE_ESP32 + +#include +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace i2s_audio { + +class I2SAudioComponent; + +class I2SAudioIn : public Parented {}; + +class I2SAudioOut : public Parented {}; + +class I2SAudioComponent : public Component { + public: + void setup() override; + + i2s_pin_config_t get_pin_config() const { + return { + .mck_io_num = this->mclk_pin_, + .bck_io_num = this->bclk_pin_, + .ws_io_num = this->lrclk_pin_, + .data_out_num = I2S_PIN_NO_CHANGE, + .data_in_num = I2S_PIN_NO_CHANGE, + }; + } + + void set_mclk_pin(int pin) { this->mclk_pin_ = pin; } + void set_bclk_pin(int pin) { this->bclk_pin_ = pin; } + void set_lrclk_pin(int pin) { this->lrclk_pin_ = pin; } + + void lock() { this->lock_.lock(); } + bool try_lock() { return this->lock_.try_lock(); } + void unlock() { this->lock_.unlock(); } + + i2s_port_t get_port() const { return this->port_; } + + protected: + Mutex lock_; + + I2SAudioIn *audio_in_{nullptr}; + I2SAudioOut *audio_out_{nullptr}; + + int mclk_pin_{I2S_PIN_NO_CHANGE}; + int bclk_pin_{I2S_PIN_NO_CHANGE}; + int lrclk_pin_; + i2s_port_t port_{}; +}; + +} // namespace i2s_audio +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py new file mode 100644 index 0000000000..600a308e6c --- /dev/null +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -0,0 +1,108 @@ +import esphome.codegen as cg +from esphome.components import media_player, esp32 +import esphome.config_validation as cv + +from esphome import pins + +from esphome.const import CONF_ID, CONF_MODE + +from .. import ( + i2s_audio_ns, + I2SAudioComponent, + I2SAudioOut, + CONF_I2S_AUDIO_ID, + CONF_I2S_DOUT_PIN, +) + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2s_audio"] + +I2SAudioMediaPlayer = i2s_audio_ns.class_( + "I2SAudioMediaPlayer", cg.Component, media_player.MediaPlayer, I2SAudioOut +) + +i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") + + +CONF_MUTE_PIN = "mute_pin" +CONF_AUDIO_ID = "audio_id" +CONF_DAC_TYPE = "dac_type" +CONF_I2S_COMM_FMT = "i2s_comm_fmt" + +INTERNAL_DAC_OPTIONS = { + "left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, + "right": i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN, + "stereo": i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN, +} + +EXTERNAL_DAC_OPTIONS = ["mono", "stereo"] + +NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2] + +I2C_COMM_FMT_OPTIONS = ["lsb", "msb"] + + +def validate_esp32_variant(config): + if config[CONF_DAC_TYPE] != "internal": + return config + variant = esp32.get_esp32_variant() + if variant in NO_INTERNAL_DAC_VARIANTS: + raise cv.Invalid(f"{variant} does not have an internal DAC") + return config + + +CONFIG_SCHEMA = cv.All( + cv.typed_schema( + { + "internal": media_player.MEDIA_PLAYER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer), + cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), + cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True), + } + ).extend(cv.COMPONENT_SCHEMA), + "external": media_player.MEDIA_PLAYER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer), + cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), + cv.Required( + CONF_I2S_DOUT_PIN + ): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_MUTE_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_MODE, default="mono"): cv.one_of( + *EXTERNAL_DAC_OPTIONS, lower=True + ), + cv.Optional(CONF_I2S_COMM_FMT, default="msb"): cv.one_of( + *I2C_COMM_FMT_OPTIONS, lower=True + ), + } + ).extend(cv.COMPONENT_SCHEMA), + }, + key=CONF_DAC_TYPE, + ), + cv.only_with_arduino, + validate_esp32_variant, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await media_player.register_media_player(var, config) + + await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) + + if config[CONF_DAC_TYPE] == "internal": + cg.add(var.set_internal_dac_mode(config[CONF_MODE])) + else: + cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) + if CONF_MUTE_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_MUTE_PIN]) + cg.add(var.set_mute_pin(pin)) + cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1)) + cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb")) + + cg.add_library("WiFiClientSecure", None) + cg.add_library("HTTPClient", None) + cg.add_library("esphome/ESP32-audioI2S", "2.0.7") + cg.add_build_flag("-DAUDIO_NO_SD_FS") diff --git a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp new file mode 100644 index 0000000000..9e2e3f136a --- /dev/null +++ b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp @@ -0,0 +1,236 @@ +#include "i2s_audio_media_player.h" + +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + +#include "esphome/core/log.h" + +namespace esphome { +namespace i2s_audio { + +static const char *const TAG = "audio"; + +void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) { + if (call.get_media_url().has_value()) { + this->current_url_ = call.get_media_url(); + + if (this->state == media_player::MEDIA_PLAYER_STATE_PLAYING && this->audio_ != nullptr) { + if (this->audio_->isRunning()) { + this->audio_->stopSong(); + } + this->audio_->connecttohost(this->current_url_.value().c_str()); + } else { + this->start(); + } + } + if (call.get_volume().has_value()) { + this->volume = call.get_volume().value(); + this->set_volume_(volume); + this->unmute_(); + } + if (this->i2s_state_ != I2S_STATE_RUNNING) { + return; + } + if (call.get_command().has_value()) { + switch (call.get_command().value()) { + case media_player::MEDIA_PLAYER_COMMAND_PLAY: + if (!this->audio_->isRunning()) + this->audio_->pauseResume(); + this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; + break; + case media_player::MEDIA_PLAYER_COMMAND_PAUSE: + if (this->audio_->isRunning()) + this->audio_->pauseResume(); + this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; + break; + case media_player::MEDIA_PLAYER_COMMAND_STOP: + this->stop(); + break; + case media_player::MEDIA_PLAYER_COMMAND_MUTE: + this->mute_(); + break; + case media_player::MEDIA_PLAYER_COMMAND_UNMUTE: + this->unmute_(); + break; + case media_player::MEDIA_PLAYER_COMMAND_TOGGLE: + this->audio_->pauseResume(); + if (this->audio_->isRunning()) { + this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; + } else { + this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; + } + break; + case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP: { + float new_volume = this->volume + 0.1f; + if (new_volume > 1.0f) + new_volume = 1.0f; + this->set_volume_(new_volume); + this->unmute_(); + break; + } + case media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: { + float new_volume = this->volume - 0.1f; + if (new_volume < 0.0f) + new_volume = 0.0f; + this->set_volume_(new_volume); + this->unmute_(); + break; + } + } + } + this->publish_state(); +} + +void I2SAudioMediaPlayer::mute_() { + if (this->mute_pin_ != nullptr) { + this->mute_pin_->digital_write(true); + } else { + this->set_volume_(0.0f, false); + } + this->muted_ = true; +} +void I2SAudioMediaPlayer::unmute_() { + if (this->mute_pin_ != nullptr) { + this->mute_pin_->digital_write(false); + } else { + this->set_volume_(this->volume, false); + } + this->muted_ = false; +} +void I2SAudioMediaPlayer::set_volume_(float volume, bool publish) { + if (this->audio_ != nullptr) + this->audio_->setVolume(remap(volume, 0.0f, 1.0f, 0, 21)); + if (publish) + this->volume = volume; +} + +void I2SAudioMediaPlayer::setup() { + ESP_LOGCONFIG(TAG, "Setting up Audio..."); + this->state = media_player::MEDIA_PLAYER_STATE_IDLE; +} + +void I2SAudioMediaPlayer::loop() { + switch (this->i2s_state_) { + case I2S_STATE_STARTING: + this->start_(); + break; + case I2S_STATE_RUNNING: + this->play_(); + break; + case I2S_STATE_STOPPING: + this->stop_(); + break; + case I2S_STATE_STOPPED: + break; + } +} + +void I2SAudioMediaPlayer::play_() { + this->audio_->loop(); + if (this->state == media_player::MEDIA_PLAYER_STATE_PLAYING && !this->audio_->isRunning()) { + this->stop(); + } +} + +void I2SAudioMediaPlayer::start() { this->i2s_state_ = I2S_STATE_STARTING; } +void I2SAudioMediaPlayer::start_() { + if (!this->parent_->try_lock()) { + return; // Waiting for another i2s to return lock + } + +#if SOC_I2S_SUPPORTS_DAC + if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) { + this->audio_ = make_unique