From 6682c43dfaeb1c006943ae546145e5f22262cadb Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 17 Apr 2019 12:06:00 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8F=97=20Merge=20C++=20into=20python=20co?= =?UTF-8?q?debase=20(#504)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: Move esphome-core codebase into esphome (and a bunch of other refactors). See https://github.com/esphome/feature-requests/issues/97 Yes this is a shit ton of work and no there's no way to automate it :( But it will be worth it 👍 Progress: - Core support (file copy etc): 80% - Base Abstractions (light, switch): ~50% - Integrations: ~10% - Working? Yes, (but only with ported components). Other refactors: - Moves all codegen related stuff into a single class: `esphome.codegen` (imported as `cg`) - Rework coroutine syntax - Move from `component/platform.py` to `domain/component.py` structure as with HA - Move all defaults out of C++ and into config validation. - Remove `make_...` helpers from Application class. Reason: Merge conflicts with every single new integration. - Pointer Variables are stored globally instead of locally in setup(). Reason: stack size limit. Future work: - Rework const.py - Move all `CONF_...` into a conf class (usage `conf.UPDATE_INTERVAL` vs `CONF_UPDATE_INTERVAL`). Reason: Less convoluted import block - Enable loading from `custom_components` folder. **Related issue (if applicable):** https://github.com/esphome/feature-requests/issues/97 **Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):** esphome/esphome-docs# ## Checklist: - [ ] The code change is tested and works locally. - [ ] Tests have been added to verify that the new code works (under `tests/` folder). If user exposed functionality or configuration variables are added/changed: - [ ] Documentation added/updated in [esphomedocs](https://github.com/OttoWinter/esphomedocs). --- .clang-format | 137 ++ .clang-tidy | 127 ++ .gitignore | 80 +- .travis.yml | 38 +- LICENSE | 692 +++++++++- MANIFEST.in | 5 +- esphome/__main__.py | 87 +- esphome/automation.py | 84 +- esphome/codegen.py | 26 + esphome/components/a4988/__init__.py | 0 esphome/components/a4988/a4988.cpp | 49 + esphome/components/a4988/a4988.h | 27 + esphome/components/a4988/stepper.py | 28 + esphome/components/adc/__init__.py | 0 esphome/components/adc/adc_sensor.cpp | 89 ++ esphome/components/adc/adc_sensor.h | 43 + esphome/components/adc/sensor.py | 52 + esphome/components/ads1115.py | 27 - esphome/components/ads1115/__init__.py | 21 + esphome/components/ads1115/ads1115.cpp | 158 +++ esphome/components/ads1115/ads1115.h | 69 + .../{sensor/ads1115.py => ads1115/sensor.py} | 47 +- esphome/components/apds9960.py | 33 - esphome/components/apds9960/__init__.py | 24 + esphome/components/apds9960/apds9960.cpp | 374 +++++ esphome/components/apds9960/apds9960.h | 62 + esphome/components/apds9960/binary_sensor.py | 27 + esphome/components/apds9960/sensor.py | 32 + .../components/{api.py => api/__init__.py} | 102 +- esphome/components/api/api.proto | 506 +++++++ esphome/components/api/api_message.cpp | 87 ++ esphome/components/api/api_message.h | 79 ++ esphome/components/api/api_server.cpp | 1202 +++++++++++++++++ esphome/components/api/api_server.h | 245 ++++ esphome/components/api/basic_messages.cpp | 57 + esphome/components/api/basic_messages.h | 63 + esphome/components/api/command_messages.cpp | 417 ++++++ esphome/components/api/command_messages.h | 162 +++ esphome/components/api/list_entities.cpp | 190 +++ esphome/components/api/list_entities.h | 57 + .../components/api/service_call_message.cpp | 49 + esphome/components/api/service_call_message.h | 53 + esphome/components/api/subscribe_logs.cpp | 26 + esphome/components/api/subscribe_logs.h | 24 + esphome/components/api/subscribe_state.cpp | 77 ++ esphome/components/api/subscribe_state.h | 70 + esphome/components/api/user_services.cpp | 74 + esphome/components/api/user_services.h | 125 ++ esphome/components/api/util.cpp | 353 +++++ esphome/components/api/util.h | 127 ++ esphome/components/bang_bang/__init__.py | 0 .../bang_bang/bang_bang_climate.cpp | 159 +++ .../components/bang_bang/bang_bang_climate.h | 89 ++ esphome/components/bang_bang/climate.py | 57 + esphome/components/bh1750/__init__.py | 0 esphome/components/bh1750/bh1750.cpp | 81 ++ esphome/components/bh1750/bh1750.h | 48 + esphome/components/bh1750/sensor.py | 32 + esphome/components/binary/__init__.py | 3 + esphome/components/binary/fan/__init__.py | 25 + esphome/components/binary/fan/binary_fan.cpp | 48 + esphome/components/binary/fan/binary_fan.h | 27 + esphome/components/binary/light/__init__.py | 18 + .../binary/light/binary_light_output.h | 32 + esphome/components/binary_sensor/__init__.py | 319 ++--- esphome/components/binary_sensor/apds9960.py | 31 - .../components/binary_sensor/automation.cpp | 113 ++ esphome/components/binary_sensor/automation.h | 161 +++ .../binary_sensor/binary_sensor.cpp | 71 + .../components/binary_sensor/binary_sensor.h | 90 ++ esphome/components/binary_sensor/custom.py | 34 - .../binary_sensor/esp32_ble_tracker.py | 24 - .../components/binary_sensor/esp32_touch.py | 56 - esphome/components/binary_sensor/filter.cpp | 68 + esphome/components/binary_sensor/filter.h | 77 ++ esphome/components/binary_sensor/gpio.py | 29 - .../components/binary_sensor/homeassistant.py | 26 - esphome/components/binary_sensor/mpr121.py | 23 - esphome/components/binary_sensor/nextion.py | 28 - esphome/components/binary_sensor/pn532.py | 44 - esphome/components/binary_sensor/rdm6300.py | 25 - .../binary_sensor/remote_receiver.py | 146 -- esphome/components/binary_sensor/status.py | 24 - esphome/components/binary_sensor/template.py | 53 - .../components/binary_sensor/ttp229_lsf.py | 23 - esphome/components/ble_presence/__init__.py | 0 .../components/ble_presence/binary_sensor.py | 24 + .../ble_presence/ble_presence_device.cpp | 16 + .../ble_presence/ble_presence_device.h | 44 + esphome/components/ble_rssi/__init__.py | 0 .../components/ble_rssi/ble_rssi_sensor.cpp | 16 + esphome/components/ble_rssi/ble_rssi_sensor.h | 42 + esphome/components/ble_rssi/sensor.py | 29 + esphome/components/bme280/__init__.py | 0 esphome/components/bme280/bme280.cpp | 318 +++++ esphome/components/bme280/bme280.h | 112 ++ esphome/components/bme280/sensor.py | 77 ++ esphome/components/bme680/__init__.py | 0 esphome/components/bme680/bme680.cpp | 483 +++++++ esphome/components/bme680/bme680.h | 141 ++ esphome/components/bme680/sensor.py | 101 ++ esphome/components/bmp085/__init__.py | 0 esphome/components/bmp085/bmp085.cpp | 138 ++ esphome/components/bmp085/bmp085.h | 47 + esphome/components/bmp085/sensor.py | 35 + esphome/components/bmp280/__init__.py | 0 esphome/components/bmp280/bmp280.cpp | 239 ++++ esphome/components/bmp280/bmp280.h | 95 ++ esphome/components/bmp280/sensor.py | 64 + esphome/components/climate/__init__.py | 110 +- esphome/components/climate/automation.h | 33 + esphome/components/climate/bang_bang.py | 65 - esphome/components/climate/climate.cpp | 259 ++++ esphome/components/climate/climate.h | 218 +++ esphome/components/climate/climate_mode.cpp | 22 + esphome/components/climate/climate_mode.h | 24 + esphome/components/climate/climate_traits.cpp | 57 + esphome/components/climate/climate_traits.h | 68 + esphome/components/cover/__init__.py | 139 +- esphome/components/cover/automation.h | 113 ++ esphome/components/cover/cover.cpp | 214 +++ esphome/components/cover/cover.h | 179 +++ esphome/components/cover/cover_traits.h | 24 + esphome/components/cover/endstop.py | 55 - esphome/components/cover/template.py | 93 -- esphome/components/cover/time_based.py | 44 - esphome/components/cse7766/__init__.py | 0 esphome/components/cse7766/cse7766.cpp | 178 +++ esphome/components/cse7766/cse7766.h | 43 + esphome/components/cse7766/sensor.py | 39 + esphome/components/custom/__init__.py | 3 + .../custom/binary_sensor/__init__.py | 26 + .../binary_sensor/custom_binary_sensor.cpp | 16 + .../binary_sensor/custom_binary_sensor.h | 24 + .../custom.py => custom/output/__init__.py} | 46 +- .../components/custom/output/custom_output.h | 31 + esphome/components/custom/sensor/__init__.py | 27 + .../custom/sensor/custom_sensor.cpp | 16 + .../components/custom/sensor/custom_sensor.h | 22 + esphome/components/custom/switch/__init__.py | 29 + .../custom/switch/custom_switch.cpp | 16 + .../components/custom/switch/custom_switch.h | 22 + .../components/custom/text_sensor/__init__.py | 29 + .../custom/text_sensor/custom_text_sensor.cpp | 16 + .../custom/text_sensor/custom_text_sensor.h | 24 + esphome/components/custom_component.py | 32 - .../components/custom_component/__init__.py | 26 + .../custom_component/custom_component.h | 26 + esphome/components/cwww/__init__.py | 0 esphome/components/cwww/cwww_light_output.h | 41 + esphome/components/cwww/light.py | 25 + esphome/components/dallas.py | 27 - esphome/components/dallas/__init__.py | 26 + .../components/dallas/dallas_component.cpp | 269 ++++ esphome/components/dallas/dallas_component.h | 78 ++ esphome/components/dallas/esp_one_wire.cpp | 217 +++ esphome/components/dallas/esp_one_wire.h | 71 + esphome/components/dallas/sensor.py | 34 + esphome/components/debug.py | 14 - esphome/components/debug/__init__.py | 17 + esphome/components/debug/debug_component.cpp | 210 +++ esphome/components/debug/debug_component.h | 19 + esphome/components/deep_sleep.py | 115 -- esphome/components/deep_sleep/__init__.py | 106 ++ .../deep_sleep/deep_sleep_component.cpp | 93 ++ .../deep_sleep/deep_sleep_component.h | 111 ++ esphome/components/dht/__init__.py | 0 esphome/components/dht/dht.cpp | 194 +++ esphome/components/dht/dht.h | 67 + esphome/components/dht/sensor.py | 50 + esphome/components/dht12/__init__.py | 0 esphome/components/dht12/dht12.cpp | 70 + esphome/components/dht12/dht12.h | 30 + esphome/components/dht12/sensor.py | 33 + esphome/components/display/__init__.py | 112 +- esphome/components/display/display_buffer.cpp | 449 ++++++ esphome/components/display/display_buffer.h | 428 ++++++ esphome/components/display/lcd_gpio.py | 70 - esphome/components/display/lcd_pcf8574.py | 39 - esphome/components/display/max7219.py | 48 - esphome/components/display/nextion.py | 34 - esphome/components/display/ssd1306_i2c.py | 46 - esphome/components/display/ssd1306_spi.py | 66 - .../components/display/waveshare_epaper.py | 87 -- esphome/components/duty_cycle/__init__.py | 0 .../duty_cycle/duty_cycle_sensor.cpp | 60 + .../components/duty_cycle/duty_cycle_sensor.h | 38 + esphome/components/duty_cycle/sensor.py | 23 + esphome/components/endstop/__init__.py | 0 esphome/components/endstop/cover.py | 46 + esphome/components/endstop/endstop_cover.cpp | 173 +++ esphome/components/endstop/endstop_cover.h | 59 + esphome/components/esp32_ble_beacon.py | 40 - .../components/esp32_ble_beacon/__init__.py | 29 + .../esp32_ble_beacon/esp32_ble_beacon.cpp | 134 ++ .../esp32_ble_beacon/esp32_ble_beacon.h | 57 + esphome/components/esp32_ble_tracker.py | 40 - .../components/esp32_ble_tracker/__init__.py | 26 + .../esp32_ble_tracker/binary_sensor.py | 3 + .../esp32_ble_tracker/esp32_ble_tracker.cpp | 480 +++++++ .../esp32_ble_tracker/esp32_ble_tracker.h | 134 ++ .../__init__.py} | 96 +- .../components/esp32_camera/esp32_camera.cpp | 306 +++++ .../components/esp32_camera/esp32_camera.h | 113 ++ esphome/components/esp32_hall/__init__.py | 0 esphome/components/esp32_hall/esp32_hall.cpp | 23 + esphome/components/esp32_hall/esp32_hall.h | 26 + esphome/components/esp32_hall/sensor.py | 25 + esphome/components/esp32_touch.py | 89 -- esphome/components/esp32_touch/__init__.py | 80 ++ .../components/esp32_touch/binary_sensor.py | 50 + .../components/esp32_touch/esp32_touch.cpp | 164 +++ esphome/components/esp32_touch/esp32_touch.h | 70 + esphome/components/esp8266_pwm/__init__.py | 0 .../components/esp8266_pwm/esp8266_pwm.cpp | 49 + esphome/components/esp8266_pwm/esp8266_pwm.h | 30 + esphome/components/esp8266_pwm/output.py | 32 + esphome/components/ethernet.py | 84 -- esphome/components/ethernet/__init__.py | 101 ++ .../ethernet/ethernet_component.cpp | 254 ++++ .../components/ethernet/ethernet_component.h | 81 ++ esphome/components/fan/__init__.py | 164 ++- esphome/components/fan/automation.cpp | 10 + esphome/components/fan/automation.h | 60 + esphome/components/fan/binary.py | 28 - esphome/components/fan/fan_state.cpp | 107 ++ esphome/components/fan/fan_state.h | 76 ++ esphome/components/fan/fan_traits.h | 26 + esphome/components/fan/speed.py | 42 - esphome/components/fastled_base/__init__.py | 46 + .../components/fastled_base/fastled_light.cpp | 61 + .../components/fastled_base/fastled_light.h | 252 ++++ .../components/fastled_clockless/__init__.py | 0 esphome/components/fastled_clockless/light.py | 55 + esphome/components/fastled_spi/__init__.py | 0 esphome/components/fastled_spi/light.py | 35 + .../components/{font.py => font/__init__.py} | 42 +- esphome/components/globals.py | 35 - esphome/components/globals/__init__.py | 39 + .../components/globals/globals_component.h | 53 + esphome/components/gpio/__init__.py | 3 + .../components/gpio/binary_sensor/__init__.py | 21 + .../gpio/binary_sensor/gpio_binary_sensor.cpp | 26 + .../gpio/binary_sensor/gpio_binary_sensor.h | 28 + esphome/components/gpio/output/__init__.py | 21 + .../gpio/output/gpio_binary_output.cpp | 16 + .../gpio/output/gpio_binary_output.h | 29 + esphome/components/gpio/switch/__init__.py | 41 + .../components/gpio/switch/gpio_switch.cpp | 90 ++ esphome/components/gpio/switch/gpio_switch.h | 39 + esphome/components/hdc1080/__init__.py | 0 esphome/components/hdc1080/hdc1080.cpp | 60 + esphome/components/hdc1080/hdc1080.h | 32 + esphome/components/hdc1080/sensor.py | 33 + esphome/components/hlw8012/__init__.py | 0 esphome/components/hlw8012/hlw8012.cpp | 91 ++ esphome/components/hlw8012/hlw8012.h | 48 + esphome/components/hlw8012/sensor.py | 57 + esphome/components/hmc5883l/__init__.py | 0 esphome/components/hmc5883l/hmc5883l.cpp | 145 ++ esphome/components/hmc5883l/hmc5883l.h | 50 + esphome/components/hmc5883l/sensor.py | 71 + esphome/components/homeassistant/__init__.py | 3 + .../homeassistant/binary_sensor/__init__.py | 20 + .../homeassistant_binary_sensor.cpp | 38 + .../homeassistant_binary_sensor.h | 21 + .../homeassistant/sensor/__init__.py | 20 + .../sensor/homeassistant_sensor.cpp | 32 + .../sensor/homeassistant_sensor.h | 21 + .../homeassistant/text_sensor/__init__.py | 21 + .../text_sensor/homeassistant_text_sensor.cpp | 22 + .../text_sensor/homeassistant_text_sensor.h | 21 + .../components/homeassistant/time/__init__.py | 20 + .../homeassistant/time/homeassistant_time.cpp | 40 + .../homeassistant/time/homeassistant_time.h | 27 + esphome/components/htu21d/__init__.py | 0 esphome/components/htu21d/htu21d.cpp | 63 + esphome/components/htu21d/htu21d.h | 30 + esphome/components/htu21d/sensor.py | 33 + esphome/components/hx711/__init__.py | 0 esphome/components/hx711/hx711.cpp | 67 + esphome/components/hx711/hx711.h | 36 + esphome/components/hx711/sensor.py | 37 + esphome/components/i2c.py | 38 - esphome/components/i2c/__init__.py | 47 + esphome/components/i2c/i2c.cpp | 205 +++ esphome/components/i2c/i2c.h | 207 +++ .../{image.py => image/__init__.py} | 24 +- esphome/components/ina219/__init__.py | 0 esphome/components/ina219/ina219.cpp | 195 +++ esphome/components/ina219/ina219.h | 38 + esphome/components/ina219/sensor.py | 51 + esphome/components/ina3221/__init__.py | 0 esphome/components/ina3221/ina3221.cpp | 133 ++ esphome/components/ina3221/ina3221.h | 41 + esphome/components/ina3221/sensor.py | 58 + esphome/components/interval.py | 24 - esphome/components/interval/__init__.py | 19 + esphome/components/interval/interval.h | 17 + esphome/components/json/__init__.py | 9 + esphome/components/json/json_util.cpp | 129 ++ esphome/components/json/json_util.h | 62 + esphome/components/lcd_base/__init__.py | 36 + esphome/components/lcd_base/lcd_display.cpp | 162 +++ esphome/components/lcd_base/lcd_display.h | 67 + esphome/components/lcd_gpio/__init__.py | 0 esphome/components/lcd_gpio/display.py | 45 + .../components/lcd_gpio/gpio_lcd_display.cpp | 59 + .../components/lcd_gpio/gpio_lcd_display.h | 48 + esphome/components/lcd_pcf8574/__init__.py | 0 esphome/components/lcd_pcf8574/display.py | 20 + .../lcd_pcf8574/pcf8574_display.cpp | 46 + .../components/lcd_pcf8574/pcf8574_display.h | 23 + esphome/components/ledc/__init__.py | 0 esphome/components/ledc/ledc_output.cpp | 44 + esphome/components/ledc/ledc_output.h | 43 + esphome/components/ledc/output.py | 39 + esphome/components/light/__init__.py | 658 ++++----- esphome/components/light/addressable_light.h | 449 ++++++ .../light/addressable_light_effect.h | 350 +++++ esphome/components/light/automation.h | 79 ++ esphome/components/light/base_light_effects.h | 143 ++ esphome/components/light/binary.py | 21 - esphome/components/light/cwww.py | 30 - esphome/components/light/fastled_clockless.py | 97 -- esphome/components/light/fastled_spi.py | 80 -- esphome/components/light/light_color_values.h | 272 ++++ esphome/components/light/light_effect.h | 43 + esphome/components/light/light_output.h | 24 + esphome/components/light/light_state.cpp | 703 ++++++++++ esphome/components/light/light_state.h | 319 +++++ esphome/components/light/light_traits.h | 38 + esphome/components/light/light_transformer.h | 86 ++ esphome/components/light/monochromatic.py | 21 - esphome/components/light/neopixelbus.py | 181 --- esphome/components/light/partition.py | 43 - esphome/components/light/rgb.py | 25 - esphome/components/light/rgbw.py | 27 - esphome/components/light/rgbww.py | 47 - esphome/components/logger.py | 183 --- esphome/components/logger/__init__.py | 185 +++ esphome/components/logger/logger.cpp | 149 ++ esphome/components/logger/logger.h | 85 ++ esphome/components/max31855/__init__.py | 0 esphome/components/max31855/max31855.cpp | 91 ++ esphome/components/max31855/max31855.h | 27 + esphome/components/max31855/sensor.py | 21 + esphome/components/max6675/__init__.py | 0 esphome/components/max6675/max6675.cpp | 57 + esphome/components/max6675/max6675.h | 27 + esphome/components/max6675/sensor.py | 21 + esphome/components/max7219/__init__.py | 0 esphome/components/max7219/display.py | 33 + esphome/components/max7219/max7219.cpp | 228 ++++ esphome/components/max7219/max7219.h | 68 + esphome/components/mcp23017.py | 35 - esphome/components/mcp23017/__init__.py | 52 + esphome/components/mcp23017/mcp23017.cpp | 96 ++ esphome/components/mcp23017/mcp23017.h | 82 ++ esphome/components/mhz19/__init__.py | 0 esphome/components/mhz19/mhz19.cpp | 73 + esphome/components/mhz19/mhz19.h | 30 + esphome/components/mhz19/sensor.py | 34 + esphome/components/monochromatic/__init__.py | 0 esphome/components/monochromatic/light.py | 18 + .../monochromatic_light_output.h | 29 + esphome/components/mpr121.py | 29 - esphome/components/mpr121/__init__.py | 22 + esphome/components/mpr121/binary_sensor.py | 21 + esphome/components/mpr121/mpr121.cpp | 76 ++ esphome/components/mpr121/mpr121.h | 75 + esphome/components/mpu6050/__init__.py | 0 esphome/components/mpu6050/mpu6050.cpp | 145 ++ esphome/components/mpu6050/mpu6050.h | 41 + esphome/components/mpu6050/sensor.py | 54 + esphome/components/mqtt.py | 295 ---- esphome/components/mqtt/__init__.py | 314 +++++ .../components/mqtt/mqtt_binary_sensor.cpp | 54 + esphome/components/mqtt/mqtt_binary_sensor.h | 44 + esphome/components/mqtt/mqtt_client.cpp | 554 ++++++++ esphome/components/mqtt/mqtt_client.h | 345 +++++ esphome/components/mqtt/mqtt_climate.cpp | 177 +++ esphome/components/mqtt/mqtt_climate.h | 45 + esphome/components/mqtt/mqtt_component.cpp | 199 +++ esphome/components/mqtt/mqtt_component.h | 179 +++ esphome/components/mqtt/mqtt_cover.cpp | 111 ++ esphome/components/mqtt/mqtt_cover.h | 42 + esphome/components/mqtt/mqtt_fan.cpp | 124 ++ esphome/components/mqtt/mqtt_fan.h | 47 + esphome/components/mqtt/mqtt_light.cpp | 60 + esphome/components/mqtt/mqtt_light.h | 41 + esphome/components/mqtt/mqtt_sensor.cpp | 77 ++ esphome/components/mqtt/mqtt_sensor.h | 60 + esphome/components/mqtt/mqtt_switch.cpp | 60 + esphome/components/mqtt/mqtt_switch.h | 41 + esphome/components/mqtt/mqtt_text_sensor.cpp | 47 + esphome/components/mqtt/mqtt_text_sensor.h | 40 + esphome/components/mqtt_subscribe/__init__.py | 3 + .../mqtt_subscribe/sensor/__init__.py | 27 + .../sensor/mqtt_subscribe_sensor.cpp | 32 + .../sensor/mqtt_subscribe_sensor.h | 28 + .../mqtt_subscribe/text_sensor/__init__.py | 28 + .../mqtt_subscribe_text_sensor.cpp | 22 + .../text_sensor/mqtt_subscribe_text_sensor.h | 27 + esphome/components/ms5611/__init__.py | 0 esphome/components/ms5611/ms5611.cpp | 111 ++ esphome/components/ms5611/ms5611.h | 32 + esphome/components/ms5611/sensor.py | 33 + esphome/components/my9231.py | 44 - esphome/components/my9231/__init__.py | 30 + esphome/components/my9231/my9231.cpp | 111 ++ esphome/components/my9231/my9231.h | 64 + esphome/components/my9231/output.py | 23 + esphome/components/neopixelbus/__init__.py | 0 esphome/components/neopixelbus/light.py | 169 +++ .../neopixelbus/neopixelbus_light.h | 180 +++ esphome/components/nextion/__init__.py | 3 + esphome/components/nextion/binary_sensor.py | 28 + esphome/components/nextion/display.py | 29 + esphome/components/nextion/nextion.cpp | 292 ++++ esphome/components/nextion/nextion.h | 234 ++++ esphome/components/ota.py | 56 - esphome/components/ota/__init__.py | 35 + esphome/components/ota/ota_component.cpp | 407 ++++++ esphome/components/ota/ota_component.h | 94 ++ esphome/components/output/__init__.py | 115 +- esphome/components/output/automation.cpp | 10 + esphome/components/output/automation.h | 52 + esphome/components/output/binary_output.h | 71 + esphome/components/output/copy.py | 56 - esphome/components/output/esp8266_pwm.py | 41 - esphome/components/output/float_output.cpp | 48 + esphome/components/output/float_output.h | 71 + esphome/components/output/gpio.py | 28 - esphome/components/output/ledc.py | 47 - esphome/components/output/my9231.py | 33 - esphome/components/output/pca9685.py | 31 - esphome/components/output/switch/__init__.py | 19 + .../output/switch/output_switch.cpp | 31 + .../components/output/switch/output_switch.h | 25 + esphome/components/partition/__init__.py | 0 esphome/components/partition/light.py | 36 + .../components/partition/light_partition.cpp | 10 + .../components/partition/light_partition.h | 84 ++ esphome/components/pca9685.py | 32 - esphome/components/pca9685/__init__.py | 22 + esphome/components/pca9685/output.py | 24 + esphome/components/pca9685/pca9685_output.cpp | 141 ++ esphome/components/pca9685/pca9685_output.h | 68 + esphome/components/pcf8574.py | 36 - esphome/components/pcf8574/__init__.py | 54 + esphome/components/pcf8574/pcf8574.cpp | 113 ++ esphome/components/pcf8574/pcf8574.h | 62 + esphome/components/pmsx003/__init__.py | 0 esphome/components/pmsx003/pmsx003.cpp | 169 +++ esphome/components/pmsx003/pmsx003.h | 49 + esphome/components/pmsx003/sensor.py | 91 ++ esphome/components/pn532.py | 44 - esphome/components/pn532/__init__.py | 32 + esphome/components/pn532/binary_sensor.py | 42 + esphome/components/pn532/pn532.cpp | 384 ++++++ esphome/components/pn532/pn532.h | 94 ++ esphome/components/power_supply.py | 35 - esphome/components/power_supply/__init__.py | 24 + .../components/power_supply/power_supply.cpp | 67 + .../components/power_supply/power_supply.h | 41 + esphome/components/pulse_counter/__init__.py | 0 .../pulse_counter/pulse_counter_sensor.cpp | 165 +++ .../pulse_counter/pulse_counter_sensor.h | 78 ++ esphome/components/pulse_counter/sensor.py | 69 + esphome/components/pulse_width/__init__.py | 0 .../components/pulse_width/pulse_width.cpp | 31 + esphome/components/pulse_width/pulse_width.h | 44 + esphome/components/pulse_width/sensor.py | 26 + esphome/components/rdm6300.py | 26 - esphome/components/rdm6300/__init__.py | 30 + esphome/components/rdm6300/binary_sensor.py | 23 + esphome/components/rdm6300/rdm6300.cpp | 69 + esphome/components/rdm6300/rdm6300.h | 55 + esphome/components/remote_base/__init__.py | 658 +++++++++ .../components/remote_base/jvc_protocol.cpp | 51 + esphome/components/remote_base/jvc_protocol.h | 34 + .../components/remote_base/lg_protocol.cpp | 57 + esphome/components/remote_base/lg_protocol.h | 38 + .../components/remote_base/nec_protocol.cpp | 72 + esphome/components/remote_base/nec_protocol.h | 37 + .../remote_base/panasonic_protocol.cpp | 72 + .../remote_base/panasonic_protocol.h | 38 + .../components/remote_base/raw_protocol.cpp | 46 + esphome/components/remote_base/raw_protocol.h | 75 + .../components/remote_base/rc5_protocol.cpp | 62 + esphome/components/remote_base/rc5_protocol.h | 38 + .../remote_base/rc_switch_protocol.cpp | 245 ++++ .../remote_base/rc_switch_protocol.h | 204 +++ .../components/remote_base/remote_base.cpp | 59 + esphome/components/remote_base/remote_base.h | 343 +++++ .../remote_base/samsung_protocol.cpp | 58 + .../components/remote_base/samsung_protocol.h | 35 + .../components/remote_base/sony_protocol.cpp | 68 + .../components/remote_base/sony_protocol.h | 38 + esphome/components/remote_receiver.py | 75 - .../components/remote_receiver/__init__.py | 38 + .../remote_receiver/binary_sensor.py | 18 + .../remote_receiver/remote_receiver.h | 59 + .../remote_receiver/remote_receiver_esp32.cpp | 154 +++ .../remote_receiver_esp8266.cpp | 123 ++ esphome/components/remote_transmitter.py | 119 -- .../components/remote_transmitter/__init__.py | 26 + .../remote_transmitter/remote_transmitter.cpp | 11 + .../remote_transmitter/remote_transmitter.h | 43 + .../remote_transmitter_esp32.cpp | 134 ++ .../remote_transmitter_esp8266.cpp | 95 ++ .../components/remote_transmitter/switch.py | 30 + esphome/components/restart/__init__.py | 0 esphome/components/restart/restart_switch.cpp | 24 + esphome/components/restart/restart_switch.h | 20 + esphome/components/restart/switch.py | 20 + esphome/components/rgb/__init__.py | 0 esphome/components/rgb/light.py | 22 + esphome/components/rgb/rgb_light_output.h | 35 + esphome/components/rgbw/__init__.py | 0 esphome/components/rgbw/light.py | 24 + esphome/components/rgbw/rgbw_light_output.h | 39 + esphome/components/rgbww/__init__.py | 0 esphome/components/rgbww/light.py | 32 + esphome/components/rgbww/rgbww_light_output.h | 51 + esphome/components/rotary_encoder/__init__.py | 0 .../rotary_encoder/rotary_encoder.cpp | 150 ++ .../rotary_encoder/rotary_encoder.h | 66 + esphome/components/rotary_encoder/sensor.py | 61 + esphome/components/script.py | 50 - esphome/components/script/__init__.py | 40 + esphome/components/script/script.h | 40 + esphome/components/sds011/__init__.py | 0 esphome/components/sds011/sds011.cpp | 175 +++ esphome/components/sds011/sds011.h | 46 + esphome/components/sds011/sensor.py | 54 + esphome/components/sensor/__init__.py | 391 +++--- esphome/components/sensor/adc.py | 62 - esphome/components/sensor/apds9960.py | 30 - esphome/components/sensor/automation.cpp | 10 + esphome/components/sensor/automation.h | 113 ++ esphome/components/sensor/bh1750.py | 41 - esphome/components/sensor/ble_rssi.py | 25 - esphome/components/sensor/bme280.py | 87 -- esphome/components/sensor/bme680.py | 110 -- esphome/components/sensor/bmp085.py | 45 - esphome/components/sensor/bmp280.py | 77 -- esphome/components/sensor/cse7766.py | 58 - esphome/components/sensor/custom.py | 32 - esphome/components/sensor/dallas.py | 33 - esphome/components/sensor/dht.py | 58 - esphome/components/sensor/dht12.py | 42 - esphome/components/sensor/duty_cycle.py | 30 - esphome/components/sensor/esp32_hall.py | 28 - esphome/components/sensor/filter.cpp | 232 ++++ esphome/components/sensor/filter.h | 247 ++++ esphome/components/sensor/hdc1080.py | 42 - esphome/components/sensor/hlw8012.py | 67 - esphome/components/sensor/hmc5883l.py | 89 -- esphome/components/sensor/homeassistant.py | 25 - esphome/components/sensor/htu21d.py | 43 - esphome/components/sensor/hx711.py | 47 - esphome/components/sensor/ina219.py | 68 - esphome/components/sensor/ina3221.py | 79 -- esphome/components/sensor/max31855.py | 33 - esphome/components/sensor/max6675.py | 34 - esphome/components/sensor/mhz19.py | 46 - esphome/components/sensor/mpu6050.py | 92 -- esphome/components/sensor/mqtt_subscribe.py | 32 - esphome/components/sensor/ms5611.py | 46 - esphome/components/sensor/pmsx003.py | 96 -- esphome/components/sensor/pulse_counter.py | 77 -- esphome/components/sensor/rotary_encoder.py | 69 - esphome/components/sensor/sds011.py | 69 - esphome/components/sensor/sensor.cpp | 118 ++ esphome/components/sensor/sensor.h | 187 +++ esphome/components/sensor/sht3xd.py | 44 - esphome/components/sensor/tcs34725.py | 104 -- esphome/components/sensor/template.py | 51 - .../components/sensor/total_daily_energy.py | 32 - esphome/components/sensor/tsl2561.py | 65 - esphome/components/sensor/ultrasonic.py | 52 - esphome/components/sensor/uptime.py | 26 - esphome/components/sensor/wifi_signal.py | 26 - esphome/components/sensor/xiaomi_miflora.py | 43 - esphome/components/sensor/xiaomi_mijia.py | 35 - esphome/components/servo.py | 56 - esphome/components/servo/__init__.py | 54 + esphome/components/servo/servo.cpp | 19 + esphome/components/servo/servo.h | 88 ++ esphome/components/sht3xd/__init__.py | 0 esphome/components/sht3xd/sensor.py | 34 + esphome/components/sht3xd/sht3xd.cpp | 121 ++ esphome/components/sht3xd/sht3xd.h | 32 + esphome/components/shutdown/__init__.py | 0 .../components/shutdown/shutdown_switch.cpp | 30 + esphome/components/shutdown/shutdown_switch.h | 20 + esphome/components/shutdown/switch.py | 21 + esphome/components/sntp/__init__.py | 0 esphome/components/sntp/sntp_component.cpp | 64 + esphome/components/sntp/sntp_component.h | 39 + esphome/components/sntp/time.py | 28 + esphome/components/speed/__init__.py | 3 + esphome/components/speed/fan/__init__.py | 32 + esphome/components/speed/fan/speed_fan.cpp | 53 + esphome/components/speed/fan/speed_fan.h | 36 + esphome/components/spi.py | 37 - esphome/components/spi/__init__.py | 46 + esphome/components/spi/spi.cpp | 104 ++ esphome/components/spi/spi.h | 75 + esphome/components/ssd1306_base/__init__.py | 49 + .../components/ssd1306_base/ssd1306_base.cpp | 238 ++++ .../components/ssd1306_base/ssd1306_base.h | 58 + esphome/components/ssd1306_i2c/__init__.py | 0 esphome/components/ssd1306_i2c/display.py | 21 + .../components/ssd1306_i2c/ssd1306_i2c.cpp | 61 + esphome/components/ssd1306_i2c/ssd1306_i2c.h | 25 + esphome/components/ssd1306_spi/__init__.py | 0 esphome/components/ssd1306_spi/display.py | 25 + .../components/ssd1306_spi/ssd1306_spi.cpp | 58 + esphome/components/ssd1306_spi/ssd1306_spi.h | 29 + esphome/components/status/__init__.py | 0 esphome/components/status/binary_sensor.py | 20 + .../status/status_binary_sensor.cpp | 40 + .../components/status/status_binary_sensor.h | 28 + esphome/components/status_led.py | 25 - esphome/components/status_led/__init__.py | 23 + esphome/components/status_led/status_led.cpp | 35 + esphome/components/status_led/status_led.h | 26 + esphome/components/stepper/__init__.py | 99 +- esphome/components/stepper/a4988.py | 35 - esphome/components/stepper/stepper.cpp | 50 + esphome/components/stepper/stepper.h | 70 + esphome/components/stepper/uln2003.py | 51 - .../__init__.py} | 10 +- esphome/components/switch/__init__.py | 160 +-- esphome/components/switch/automation.cpp | 10 + esphome/components/switch/automation.h | 95 ++ esphome/components/switch/custom.py | 33 - esphome/components/switch/gpio.py | 48 - esphome/components/switch/output.py | 27 - .../components/switch/remote_transmitter.py | 165 --- esphome/components/switch/restart.py | 23 - esphome/components/switch/shutdown.py | 23 - esphome/components/switch/switch.cpp | 58 + esphome/components/switch/switch.h | 126 ++ esphome/components/switch/template.py | 71 - esphome/components/switch/uart.py | 45 - esphome/components/tcs34725/__init__.py | 0 esphome/components/tcs34725/sensor.py | 81 ++ esphome/components/tcs34725/tcs34725.cpp | 123 ++ esphome/components/tcs34725/tcs34725.h | 59 + esphome/components/template/__init__.py | 3 + .../template/binary_sensor/__init__.py | 39 + .../binary_sensor/template_binary_sensor.cpp | 21 + .../binary_sensor/template_binary_sensor.h | 25 + esphome/components/template/cover/__init__.py | 75 + .../template/cover/template_cover.cpp | 137 ++ .../template/cover/template_cover.h | 60 + .../components/template/sensor/__init__.py | 45 + .../template/sensor/template_sensor.cpp | 28 + .../template/sensor/template_sensor.h | 26 + .../components/template/switch/__init__.py | 54 + .../template/switch/template_switch.cpp | 66 + .../template/switch/template_switch.h | 42 + .../template/text_sensor/__init__.py | 42 + .../text_sensor/template_text_sensor.cpp | 25 + .../text_sensor/template_text_sensor.h | 27 + esphome/components/text_sensor/__init__.py | 70 +- esphome/components/text_sensor/automation.cpp | 10 + esphome/components/text_sensor/automation.h | 31 + esphome/components/text_sensor/custom.py | 33 - .../components/text_sensor/homeassistant.py | 26 - .../components/text_sensor/mqtt_subscribe.py | 33 - esphome/components/text_sensor/template.py | 52 - .../components/text_sensor/text_sensor.cpp | 33 + esphome/components/text_sensor/text_sensor.h | 52 + esphome/components/text_sensor/version.py | 23 - esphome/components/text_sensor/wifi_info.py | 45 - esphome/components/time/__init__.py | 195 ++- esphome/components/time/automation.cpp | 79 ++ esphome/components/time/automation.h | 41 + esphome/components/time/homeassistant.py | 24 - esphome/components/time/real_time_clock.cpp | 138 ++ esphome/components/time/real_time_clock.h | 99 ++ esphome/components/time/sntp.py | 30 - esphome/components/time_based/__init__.py | 0 esphome/components/time_based/cover.py | 35 + .../time_based/time_based_cover.cpp | 141 ++ .../components/time_based/time_based_cover.h | 48 + .../components/total_daily_energy/__init__.py | 0 .../components/total_daily_energy/sensor.py | 25 + .../total_daily_energy/total_daily_energy.cpp | 54 + .../total_daily_energy/total_daily_energy.h | 37 + esphome/components/tsl2561/__init__.py | 0 esphome/components/tsl2561/sensor.py | 49 + esphome/components/tsl2561/tsl2561.cpp | 166 +++ esphome/components/tsl2561/tsl2561.h | 89 ++ esphome/components/ttp229_lsf.py | 25 - esphome/components/ttp229_lsf/__init__.py | 22 + .../components/ttp229_lsf/binary_sensor.py | 21 + esphome/components/ttp229_lsf/ttp229.cpp | 43 + esphome/components/ttp229_lsf/ttp229.h | 36 + esphome/components/uart.py | 43 - esphome/components/uart/__init__.py | 46 + esphome/components/uart/switch/__init__.py | 38 + .../components/uart/switch/uart_switch.cpp | 26 + esphome/components/uart/switch/uart_switch.h | 22 + esphome/components/uart/uart.cpp | 350 +++++ esphome/components/uart/uart.h | 121 ++ esphome/components/uln2003/__init__.py | 0 esphome/components/uln2003/stepper.py | 41 + esphome/components/uln2003/uln2003.cpp | 97 ++ esphome/components/uln2003/uln2003.h | 42 + esphome/components/ultrasonic/__init__.py | 0 esphome/components/ultrasonic/sensor.py | 40 + .../ultrasonic/ultrasonic_sensor.cpp | 56 + .../components/ultrasonic/ultrasonic_sensor.h | 48 + esphome/components/uptime/__init__.py | 0 esphome/components/uptime/sensor.py | 24 + esphome/components/uptime/uptime_sensor.cpp | 34 + esphome/components/uptime/uptime_sensor.h | 24 + esphome/components/version/__init__.py | 0 esphome/components/version/text_sensor.py | 18 + .../version/version_text_sensor.cpp | 18 + .../components/version/version_text_sensor.h | 19 + .../components/waveshare_epaper/__init__.py | 0 .../components/waveshare_epaper/display.py | 81 ++ .../waveshare_epaper/waveshare_epaper.cpp | 623 +++++++++ .../waveshare_epaper/waveshare_epaper.h | 132 ++ esphome/components/web_server.py | 39 - esphome/components/web_server/__init__.py | 28 + esphome/components/web_server/web_server.cpp | 632 +++++++++ esphome/components/web_server/web_server.h | 142 ++ esphome/components/wifi.py | 203 --- esphome/components/wifi/__init__.py | 194 +++ esphome/components/wifi/wifi_component.cpp | 545 ++++++++ esphome/components/wifi/wifi_component.h | 236 ++++ .../components/wifi/wifi_component_esp32.cpp | 527 ++++++++ .../wifi/wifi_component_esp8266.cpp | 594 ++++++++ esphome/components/wifi_info/__init__.py | 0 esphome/components/wifi_info/text_sensor.py | 39 + .../wifi_info/wifi_info_text_sensor.h | 61 + esphome/components/wifi_signal/__init__.py | 0 esphome/components/wifi_signal/sensor.py | 24 + .../wifi_signal/wifi_signal_sensor.cpp | 12 + .../wifi_signal/wifi_signal_sensor.h | 24 + esphome/components/xiaomi_ble/__init__.py | 20 + esphome/components/xiaomi_ble/xiaomi_ble.cpp | 147 ++ esphome/components/xiaomi_ble/xiaomi_ble.h | 35 + esphome/components/xiaomi_miflora/__init__.py | 0 esphome/components/xiaomi_miflora/sensor.py | 69 + .../xiaomi_miflora/xiaomi_miflora.cpp | 23 + .../xiaomi_miflora/xiaomi_miflora.h | 59 + esphome/components/xiaomi_mijia/__init__.py | 0 esphome/components/xiaomi_mijia/sensor.py | 50 + .../components/xiaomi_mijia/xiaomi_mijia.cpp | 21 + .../components/xiaomi_mijia/xiaomi_mijia.h | 51 + esphome/config.py | 203 ++- esphome/config_validation.py | 429 +++--- esphome/const.py | 912 +++++++------ esphome/core.py | 300 +++- esphome/core/application.cpp | 113 ++ esphome/core/application.h | 271 ++++ esphome/core/automation.cpp | 33 + esphome/core/automation.h | 241 ++++ esphome/core/automation.tcc | 243 ++++ esphome/core/component.cpp | 251 ++++ esphome/core/component.h | 298 ++++ esphome/core/controller.cpp | 58 + esphome/core/controller.h | 60 + esphome/core/defines.h | 27 + esphome/core/esphal.cpp | 280 ++++ esphome/core/esphal.h | 118 ++ esphome/core/helpers.cpp | 307 +++++ esphome/core/helpers.h | 256 ++++ esphome/core/log.cpp | 67 + esphome/core/log.h | 173 +++ esphome/core/optional.h | 214 +++ esphome/core/preferences.cpp | 233 ++++ esphome/core/preferences.h | 92 ++ esphome/core/util.cpp | 120 ++ esphome/core/util.h | 18 + esphome/core_config.py | 252 ++-- esphome/cpp_generator.py | 204 ++- esphome/cpp_helpers.py | 82 +- esphome/cpp_types.py | 5 +- esphome/espota.py | 344 ----- esphome/espota2.py | 12 - esphome/helpers.py | 39 + esphome/pins.py | 135 +- esphome/platformio_api.py | 49 + esphome/py_compat.py | 4 +- esphome/storage_json.py | 22 +- esphome/util.py | 29 + esphome/voluptuous_schema.py | 2 + esphome/wizard.py | 9 +- esphome/writer.py | 360 ++--- esphome/yaml_util.py | 6 +- platformio.ini | 48 + requirements.txt | 3 +- requirements_test.txt | 18 + script/.neopixelbus.patch | 26 + script/ci-custom.py | 53 + script/ci-suggest-changes | 21 + script/clang-format.py | 164 +++ script/clang-tidy.py | 281 ++++ script/lint-cpp | 16 + script/lint-python | 10 + script/setup | 8 + script/test | 11 + tests/livingroom32.cpp | 28 + tests/livingroom8266.cpp | 34 + tests/test1.yaml | 195 ++- tests/test2.yaml | 9 +- tests/test3.yaml | 38 - 817 files changed, 54156 insertions(+), 10830 deletions(-) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100644 esphome/codegen.py create mode 100644 esphome/components/a4988/__init__.py create mode 100644 esphome/components/a4988/a4988.cpp create mode 100644 esphome/components/a4988/a4988.h create mode 100644 esphome/components/a4988/stepper.py create mode 100644 esphome/components/adc/__init__.py create mode 100644 esphome/components/adc/adc_sensor.cpp create mode 100644 esphome/components/adc/adc_sensor.h create mode 100644 esphome/components/adc/sensor.py delete mode 100644 esphome/components/ads1115.py create mode 100644 esphome/components/ads1115/__init__.py create mode 100644 esphome/components/ads1115/ads1115.cpp create mode 100644 esphome/components/ads1115/ads1115.h rename esphome/components/{sensor/ads1115.py => ads1115/sensor.py} (54%) delete mode 100644 esphome/components/apds9960.py create mode 100644 esphome/components/apds9960/__init__.py create mode 100644 esphome/components/apds9960/apds9960.cpp create mode 100644 esphome/components/apds9960/apds9960.h create mode 100644 esphome/components/apds9960/binary_sensor.py create mode 100644 esphome/components/apds9960/sensor.py rename esphome/components/{api.py => api/__init__.py} (56%) create mode 100644 esphome/components/api/api.proto create mode 100644 esphome/components/api/api_message.cpp create mode 100644 esphome/components/api/api_message.h create mode 100644 esphome/components/api/api_server.cpp create mode 100644 esphome/components/api/api_server.h create mode 100644 esphome/components/api/basic_messages.cpp create mode 100644 esphome/components/api/basic_messages.h create mode 100644 esphome/components/api/command_messages.cpp create mode 100644 esphome/components/api/command_messages.h create mode 100644 esphome/components/api/list_entities.cpp create mode 100644 esphome/components/api/list_entities.h create mode 100644 esphome/components/api/service_call_message.cpp create mode 100644 esphome/components/api/service_call_message.h create mode 100644 esphome/components/api/subscribe_logs.cpp create mode 100644 esphome/components/api/subscribe_logs.h create mode 100644 esphome/components/api/subscribe_state.cpp create mode 100644 esphome/components/api/subscribe_state.h create mode 100644 esphome/components/api/user_services.cpp create mode 100644 esphome/components/api/user_services.h create mode 100644 esphome/components/api/util.cpp create mode 100644 esphome/components/api/util.h create mode 100644 esphome/components/bang_bang/__init__.py create mode 100644 esphome/components/bang_bang/bang_bang_climate.cpp create mode 100644 esphome/components/bang_bang/bang_bang_climate.h create mode 100644 esphome/components/bang_bang/climate.py create mode 100644 esphome/components/bh1750/__init__.py create mode 100644 esphome/components/bh1750/bh1750.cpp create mode 100644 esphome/components/bh1750/bh1750.h create mode 100644 esphome/components/bh1750/sensor.py create mode 100644 esphome/components/binary/__init__.py create mode 100644 esphome/components/binary/fan/__init__.py create mode 100644 esphome/components/binary/fan/binary_fan.cpp create mode 100644 esphome/components/binary/fan/binary_fan.h create mode 100644 esphome/components/binary/light/__init__.py create mode 100644 esphome/components/binary/light/binary_light_output.h delete mode 100644 esphome/components/binary_sensor/apds9960.py create mode 100644 esphome/components/binary_sensor/automation.cpp create mode 100644 esphome/components/binary_sensor/automation.h create mode 100644 esphome/components/binary_sensor/binary_sensor.cpp create mode 100644 esphome/components/binary_sensor/binary_sensor.h delete mode 100644 esphome/components/binary_sensor/custom.py delete mode 100644 esphome/components/binary_sensor/esp32_ble_tracker.py delete mode 100644 esphome/components/binary_sensor/esp32_touch.py create mode 100644 esphome/components/binary_sensor/filter.cpp create mode 100644 esphome/components/binary_sensor/filter.h delete mode 100644 esphome/components/binary_sensor/gpio.py delete mode 100644 esphome/components/binary_sensor/homeassistant.py delete mode 100644 esphome/components/binary_sensor/mpr121.py delete mode 100644 esphome/components/binary_sensor/nextion.py delete mode 100644 esphome/components/binary_sensor/pn532.py delete mode 100644 esphome/components/binary_sensor/rdm6300.py delete mode 100644 esphome/components/binary_sensor/remote_receiver.py delete mode 100644 esphome/components/binary_sensor/status.py delete mode 100644 esphome/components/binary_sensor/template.py delete mode 100644 esphome/components/binary_sensor/ttp229_lsf.py create mode 100644 esphome/components/ble_presence/__init__.py create mode 100644 esphome/components/ble_presence/binary_sensor.py create mode 100644 esphome/components/ble_presence/ble_presence_device.cpp create mode 100644 esphome/components/ble_presence/ble_presence_device.h create mode 100644 esphome/components/ble_rssi/__init__.py create mode 100644 esphome/components/ble_rssi/ble_rssi_sensor.cpp create mode 100644 esphome/components/ble_rssi/ble_rssi_sensor.h create mode 100644 esphome/components/ble_rssi/sensor.py create mode 100644 esphome/components/bme280/__init__.py create mode 100644 esphome/components/bme280/bme280.cpp create mode 100644 esphome/components/bme280/bme280.h create mode 100644 esphome/components/bme280/sensor.py create mode 100644 esphome/components/bme680/__init__.py create mode 100644 esphome/components/bme680/bme680.cpp create mode 100644 esphome/components/bme680/bme680.h create mode 100644 esphome/components/bme680/sensor.py create mode 100644 esphome/components/bmp085/__init__.py create mode 100644 esphome/components/bmp085/bmp085.cpp create mode 100644 esphome/components/bmp085/bmp085.h create mode 100644 esphome/components/bmp085/sensor.py create mode 100644 esphome/components/bmp280/__init__.py create mode 100644 esphome/components/bmp280/bmp280.cpp create mode 100644 esphome/components/bmp280/bmp280.h create mode 100644 esphome/components/bmp280/sensor.py create mode 100644 esphome/components/climate/automation.h delete mode 100644 esphome/components/climate/bang_bang.py create mode 100644 esphome/components/climate/climate.cpp create mode 100644 esphome/components/climate/climate.h create mode 100644 esphome/components/climate/climate_mode.cpp create mode 100644 esphome/components/climate/climate_mode.h create mode 100644 esphome/components/climate/climate_traits.cpp create mode 100644 esphome/components/climate/climate_traits.h create mode 100644 esphome/components/cover/automation.h create mode 100644 esphome/components/cover/cover.cpp create mode 100644 esphome/components/cover/cover.h create mode 100644 esphome/components/cover/cover_traits.h delete mode 100644 esphome/components/cover/endstop.py delete mode 100644 esphome/components/cover/template.py delete mode 100644 esphome/components/cover/time_based.py create mode 100644 esphome/components/cse7766/__init__.py create mode 100644 esphome/components/cse7766/cse7766.cpp create mode 100644 esphome/components/cse7766/cse7766.h create mode 100644 esphome/components/cse7766/sensor.py create mode 100644 esphome/components/custom/__init__.py create mode 100644 esphome/components/custom/binary_sensor/__init__.py create mode 100644 esphome/components/custom/binary_sensor/custom_binary_sensor.cpp create mode 100644 esphome/components/custom/binary_sensor/custom_binary_sensor.h rename esphome/components/{output/custom.py => custom/output/__init__.py} (52%) create mode 100644 esphome/components/custom/output/custom_output.h create mode 100644 esphome/components/custom/sensor/__init__.py create mode 100644 esphome/components/custom/sensor/custom_sensor.cpp create mode 100644 esphome/components/custom/sensor/custom_sensor.h create mode 100644 esphome/components/custom/switch/__init__.py create mode 100644 esphome/components/custom/switch/custom_switch.cpp create mode 100644 esphome/components/custom/switch/custom_switch.h create mode 100644 esphome/components/custom/text_sensor/__init__.py create mode 100644 esphome/components/custom/text_sensor/custom_text_sensor.cpp create mode 100644 esphome/components/custom/text_sensor/custom_text_sensor.h delete mode 100644 esphome/components/custom_component.py create mode 100644 esphome/components/custom_component/__init__.py create mode 100644 esphome/components/custom_component/custom_component.h create mode 100644 esphome/components/cwww/__init__.py create mode 100644 esphome/components/cwww/cwww_light_output.h create mode 100644 esphome/components/cwww/light.py delete mode 100644 esphome/components/dallas.py create mode 100644 esphome/components/dallas/__init__.py create mode 100644 esphome/components/dallas/dallas_component.cpp create mode 100644 esphome/components/dallas/dallas_component.h create mode 100644 esphome/components/dallas/esp_one_wire.cpp create mode 100644 esphome/components/dallas/esp_one_wire.h create mode 100644 esphome/components/dallas/sensor.py delete mode 100644 esphome/components/debug.py create mode 100644 esphome/components/debug/__init__.py create mode 100644 esphome/components/debug/debug_component.cpp create mode 100644 esphome/components/debug/debug_component.h delete mode 100644 esphome/components/deep_sleep.py create mode 100644 esphome/components/deep_sleep/__init__.py create mode 100644 esphome/components/deep_sleep/deep_sleep_component.cpp create mode 100644 esphome/components/deep_sleep/deep_sleep_component.h create mode 100644 esphome/components/dht/__init__.py create mode 100644 esphome/components/dht/dht.cpp create mode 100644 esphome/components/dht/dht.h create mode 100644 esphome/components/dht/sensor.py create mode 100644 esphome/components/dht12/__init__.py create mode 100644 esphome/components/dht12/dht12.cpp create mode 100644 esphome/components/dht12/dht12.h create mode 100644 esphome/components/dht12/sensor.py create mode 100644 esphome/components/display/display_buffer.cpp create mode 100644 esphome/components/display/display_buffer.h delete mode 100644 esphome/components/display/lcd_gpio.py delete mode 100644 esphome/components/display/lcd_pcf8574.py delete mode 100644 esphome/components/display/max7219.py delete mode 100644 esphome/components/display/nextion.py delete mode 100644 esphome/components/display/ssd1306_i2c.py delete mode 100644 esphome/components/display/ssd1306_spi.py delete mode 100644 esphome/components/display/waveshare_epaper.py create mode 100644 esphome/components/duty_cycle/__init__.py create mode 100644 esphome/components/duty_cycle/duty_cycle_sensor.cpp create mode 100644 esphome/components/duty_cycle/duty_cycle_sensor.h create mode 100644 esphome/components/duty_cycle/sensor.py create mode 100644 esphome/components/endstop/__init__.py create mode 100644 esphome/components/endstop/cover.py create mode 100644 esphome/components/endstop/endstop_cover.cpp create mode 100644 esphome/components/endstop/endstop_cover.h delete mode 100644 esphome/components/esp32_ble_beacon.py create mode 100644 esphome/components/esp32_ble_beacon/__init__.py create mode 100644 esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp create mode 100644 esphome/components/esp32_ble_beacon/esp32_ble_beacon.h delete mode 100644 esphome/components/esp32_ble_tracker.py create mode 100644 esphome/components/esp32_ble_tracker/__init__.py create mode 100644 esphome/components/esp32_ble_tracker/binary_sensor.py create mode 100644 esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp create mode 100644 esphome/components/esp32_ble_tracker/esp32_ble_tracker.h rename esphome/components/{esp32_camera.py => esp32_camera/__init__.py} (52%) create mode 100644 esphome/components/esp32_camera/esp32_camera.cpp create mode 100644 esphome/components/esp32_camera/esp32_camera.h create mode 100644 esphome/components/esp32_hall/__init__.py create mode 100644 esphome/components/esp32_hall/esp32_hall.cpp create mode 100644 esphome/components/esp32_hall/esp32_hall.h create mode 100644 esphome/components/esp32_hall/sensor.py delete mode 100644 esphome/components/esp32_touch.py create mode 100644 esphome/components/esp32_touch/__init__.py create mode 100644 esphome/components/esp32_touch/binary_sensor.py create mode 100644 esphome/components/esp32_touch/esp32_touch.cpp create mode 100644 esphome/components/esp32_touch/esp32_touch.h create mode 100644 esphome/components/esp8266_pwm/__init__.py create mode 100644 esphome/components/esp8266_pwm/esp8266_pwm.cpp create mode 100644 esphome/components/esp8266_pwm/esp8266_pwm.h create mode 100644 esphome/components/esp8266_pwm/output.py delete mode 100644 esphome/components/ethernet.py create mode 100644 esphome/components/ethernet/__init__.py create mode 100644 esphome/components/ethernet/ethernet_component.cpp create mode 100644 esphome/components/ethernet/ethernet_component.h create mode 100644 esphome/components/fan/automation.cpp create mode 100644 esphome/components/fan/automation.h delete mode 100644 esphome/components/fan/binary.py create mode 100644 esphome/components/fan/fan_state.cpp create mode 100644 esphome/components/fan/fan_state.h create mode 100644 esphome/components/fan/fan_traits.h delete mode 100644 esphome/components/fan/speed.py create mode 100644 esphome/components/fastled_base/__init__.py create mode 100644 esphome/components/fastled_base/fastled_light.cpp create mode 100644 esphome/components/fastled_base/fastled_light.h create mode 100644 esphome/components/fastled_clockless/__init__.py create mode 100644 esphome/components/fastled_clockless/light.py create mode 100644 esphome/components/fastled_spi/__init__.py create mode 100644 esphome/components/fastled_spi/light.py rename esphome/components/{font.py => font/__init__.py} (64%) delete mode 100644 esphome/components/globals.py create mode 100644 esphome/components/globals/__init__.py create mode 100644 esphome/components/globals/globals_component.h create mode 100644 esphome/components/gpio/__init__.py create mode 100644 esphome/components/gpio/binary_sensor/__init__.py create mode 100644 esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp create mode 100644 esphome/components/gpio/binary_sensor/gpio_binary_sensor.h create mode 100644 esphome/components/gpio/output/__init__.py create mode 100644 esphome/components/gpio/output/gpio_binary_output.cpp create mode 100644 esphome/components/gpio/output/gpio_binary_output.h create mode 100644 esphome/components/gpio/switch/__init__.py create mode 100644 esphome/components/gpio/switch/gpio_switch.cpp create mode 100644 esphome/components/gpio/switch/gpio_switch.h create mode 100644 esphome/components/hdc1080/__init__.py create mode 100644 esphome/components/hdc1080/hdc1080.cpp create mode 100644 esphome/components/hdc1080/hdc1080.h create mode 100644 esphome/components/hdc1080/sensor.py create mode 100644 esphome/components/hlw8012/__init__.py create mode 100644 esphome/components/hlw8012/hlw8012.cpp create mode 100644 esphome/components/hlw8012/hlw8012.h create mode 100644 esphome/components/hlw8012/sensor.py create mode 100644 esphome/components/hmc5883l/__init__.py create mode 100644 esphome/components/hmc5883l/hmc5883l.cpp create mode 100644 esphome/components/hmc5883l/hmc5883l.h create mode 100644 esphome/components/hmc5883l/sensor.py create mode 100644 esphome/components/homeassistant/__init__.py create mode 100644 esphome/components/homeassistant/binary_sensor/__init__.py create mode 100644 esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp create mode 100644 esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h create mode 100644 esphome/components/homeassistant/sensor/__init__.py create mode 100644 esphome/components/homeassistant/sensor/homeassistant_sensor.cpp create mode 100644 esphome/components/homeassistant/sensor/homeassistant_sensor.h create mode 100644 esphome/components/homeassistant/text_sensor/__init__.py create mode 100644 esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp create mode 100644 esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h create mode 100644 esphome/components/homeassistant/time/__init__.py create mode 100644 esphome/components/homeassistant/time/homeassistant_time.cpp create mode 100644 esphome/components/homeassistant/time/homeassistant_time.h create mode 100644 esphome/components/htu21d/__init__.py create mode 100644 esphome/components/htu21d/htu21d.cpp create mode 100644 esphome/components/htu21d/htu21d.h create mode 100644 esphome/components/htu21d/sensor.py create mode 100644 esphome/components/hx711/__init__.py create mode 100644 esphome/components/hx711/hx711.cpp create mode 100644 esphome/components/hx711/hx711.h create mode 100644 esphome/components/hx711/sensor.py delete mode 100644 esphome/components/i2c.py create mode 100644 esphome/components/i2c/__init__.py create mode 100644 esphome/components/i2c/i2c.cpp create mode 100644 esphome/components/i2c/i2c.h rename esphome/components/{image.py => image/__init__.py} (68%) create mode 100644 esphome/components/ina219/__init__.py create mode 100644 esphome/components/ina219/ina219.cpp create mode 100644 esphome/components/ina219/ina219.h create mode 100644 esphome/components/ina219/sensor.py create mode 100644 esphome/components/ina3221/__init__.py create mode 100644 esphome/components/ina3221/ina3221.cpp create mode 100644 esphome/components/ina3221/ina3221.h create mode 100644 esphome/components/ina3221/sensor.py delete mode 100644 esphome/components/interval.py create mode 100644 esphome/components/interval/__init__.py create mode 100644 esphome/components/interval/interval.h create mode 100644 esphome/components/json/__init__.py create mode 100644 esphome/components/json/json_util.cpp create mode 100644 esphome/components/json/json_util.h create mode 100644 esphome/components/lcd_base/__init__.py create mode 100644 esphome/components/lcd_base/lcd_display.cpp create mode 100644 esphome/components/lcd_base/lcd_display.h create mode 100644 esphome/components/lcd_gpio/__init__.py create mode 100644 esphome/components/lcd_gpio/display.py create mode 100644 esphome/components/lcd_gpio/gpio_lcd_display.cpp create mode 100644 esphome/components/lcd_gpio/gpio_lcd_display.h create mode 100644 esphome/components/lcd_pcf8574/__init__.py create mode 100644 esphome/components/lcd_pcf8574/display.py create mode 100644 esphome/components/lcd_pcf8574/pcf8574_display.cpp create mode 100644 esphome/components/lcd_pcf8574/pcf8574_display.h create mode 100644 esphome/components/ledc/__init__.py create mode 100644 esphome/components/ledc/ledc_output.cpp create mode 100644 esphome/components/ledc/ledc_output.h create mode 100644 esphome/components/ledc/output.py create mode 100644 esphome/components/light/addressable_light.h create mode 100644 esphome/components/light/addressable_light_effect.h create mode 100644 esphome/components/light/automation.h create mode 100644 esphome/components/light/base_light_effects.h delete mode 100644 esphome/components/light/binary.py delete mode 100644 esphome/components/light/cwww.py delete mode 100644 esphome/components/light/fastled_clockless.py delete mode 100644 esphome/components/light/fastled_spi.py create mode 100644 esphome/components/light/light_color_values.h create mode 100644 esphome/components/light/light_effect.h create mode 100644 esphome/components/light/light_output.h create mode 100644 esphome/components/light/light_state.cpp create mode 100644 esphome/components/light/light_state.h create mode 100644 esphome/components/light/light_traits.h create mode 100644 esphome/components/light/light_transformer.h delete mode 100644 esphome/components/light/monochromatic.py delete mode 100644 esphome/components/light/neopixelbus.py delete mode 100644 esphome/components/light/partition.py delete mode 100644 esphome/components/light/rgb.py delete mode 100644 esphome/components/light/rgbw.py delete mode 100644 esphome/components/light/rgbww.py delete mode 100644 esphome/components/logger.py create mode 100644 esphome/components/logger/__init__.py create mode 100644 esphome/components/logger/logger.cpp create mode 100644 esphome/components/logger/logger.h create mode 100644 esphome/components/max31855/__init__.py create mode 100644 esphome/components/max31855/max31855.cpp create mode 100644 esphome/components/max31855/max31855.h create mode 100644 esphome/components/max31855/sensor.py create mode 100644 esphome/components/max6675/__init__.py create mode 100644 esphome/components/max6675/max6675.cpp create mode 100644 esphome/components/max6675/max6675.h create mode 100644 esphome/components/max6675/sensor.py create mode 100644 esphome/components/max7219/__init__.py create mode 100644 esphome/components/max7219/display.py create mode 100644 esphome/components/max7219/max7219.cpp create mode 100644 esphome/components/max7219/max7219.h delete mode 100644 esphome/components/mcp23017.py create mode 100644 esphome/components/mcp23017/__init__.py create mode 100644 esphome/components/mcp23017/mcp23017.cpp create mode 100644 esphome/components/mcp23017/mcp23017.h create mode 100644 esphome/components/mhz19/__init__.py create mode 100644 esphome/components/mhz19/mhz19.cpp create mode 100644 esphome/components/mhz19/mhz19.h create mode 100644 esphome/components/mhz19/sensor.py create mode 100644 esphome/components/monochromatic/__init__.py create mode 100644 esphome/components/monochromatic/light.py create mode 100644 esphome/components/monochromatic/monochromatic_light_output.h delete mode 100644 esphome/components/mpr121.py create mode 100644 esphome/components/mpr121/__init__.py create mode 100644 esphome/components/mpr121/binary_sensor.py create mode 100644 esphome/components/mpr121/mpr121.cpp create mode 100644 esphome/components/mpr121/mpr121.h create mode 100644 esphome/components/mpu6050/__init__.py create mode 100644 esphome/components/mpu6050/mpu6050.cpp create mode 100644 esphome/components/mpu6050/mpu6050.h create mode 100644 esphome/components/mpu6050/sensor.py delete mode 100644 esphome/components/mqtt.py create mode 100644 esphome/components/mqtt/__init__.py create mode 100644 esphome/components/mqtt/mqtt_binary_sensor.cpp create mode 100644 esphome/components/mqtt/mqtt_binary_sensor.h create mode 100644 esphome/components/mqtt/mqtt_client.cpp create mode 100644 esphome/components/mqtt/mqtt_client.h create mode 100644 esphome/components/mqtt/mqtt_climate.cpp create mode 100644 esphome/components/mqtt/mqtt_climate.h create mode 100644 esphome/components/mqtt/mqtt_component.cpp create mode 100644 esphome/components/mqtt/mqtt_component.h create mode 100644 esphome/components/mqtt/mqtt_cover.cpp create mode 100644 esphome/components/mqtt/mqtt_cover.h create mode 100644 esphome/components/mqtt/mqtt_fan.cpp create mode 100644 esphome/components/mqtt/mqtt_fan.h create mode 100644 esphome/components/mqtt/mqtt_light.cpp create mode 100644 esphome/components/mqtt/mqtt_light.h create mode 100644 esphome/components/mqtt/mqtt_sensor.cpp create mode 100644 esphome/components/mqtt/mqtt_sensor.h create mode 100644 esphome/components/mqtt/mqtt_switch.cpp create mode 100644 esphome/components/mqtt/mqtt_switch.h create mode 100644 esphome/components/mqtt/mqtt_text_sensor.cpp create mode 100644 esphome/components/mqtt/mqtt_text_sensor.h create mode 100644 esphome/components/mqtt_subscribe/__init__.py create mode 100644 esphome/components/mqtt_subscribe/sensor/__init__.py create mode 100644 esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp create mode 100644 esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.h create mode 100644 esphome/components/mqtt_subscribe/text_sensor/__init__.py create mode 100644 esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.cpp create mode 100644 esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.h create mode 100644 esphome/components/ms5611/__init__.py create mode 100644 esphome/components/ms5611/ms5611.cpp create mode 100644 esphome/components/ms5611/ms5611.h create mode 100644 esphome/components/ms5611/sensor.py delete mode 100644 esphome/components/my9231.py create mode 100644 esphome/components/my9231/__init__.py create mode 100644 esphome/components/my9231/my9231.cpp create mode 100644 esphome/components/my9231/my9231.h create mode 100644 esphome/components/my9231/output.py create mode 100644 esphome/components/neopixelbus/__init__.py create mode 100644 esphome/components/neopixelbus/light.py create mode 100644 esphome/components/neopixelbus/neopixelbus_light.h create mode 100644 esphome/components/nextion/__init__.py create mode 100644 esphome/components/nextion/binary_sensor.py create mode 100644 esphome/components/nextion/display.py create mode 100644 esphome/components/nextion/nextion.cpp create mode 100644 esphome/components/nextion/nextion.h delete mode 100644 esphome/components/ota.py create mode 100644 esphome/components/ota/__init__.py create mode 100644 esphome/components/ota/ota_component.cpp create mode 100644 esphome/components/ota/ota_component.h create mode 100644 esphome/components/output/automation.cpp create mode 100644 esphome/components/output/automation.h create mode 100644 esphome/components/output/binary_output.h delete mode 100644 esphome/components/output/copy.py delete mode 100644 esphome/components/output/esp8266_pwm.py create mode 100644 esphome/components/output/float_output.cpp create mode 100644 esphome/components/output/float_output.h delete mode 100644 esphome/components/output/gpio.py delete mode 100644 esphome/components/output/ledc.py delete mode 100644 esphome/components/output/my9231.py delete mode 100644 esphome/components/output/pca9685.py create mode 100644 esphome/components/output/switch/__init__.py create mode 100644 esphome/components/output/switch/output_switch.cpp create mode 100644 esphome/components/output/switch/output_switch.h create mode 100644 esphome/components/partition/__init__.py create mode 100644 esphome/components/partition/light.py create mode 100644 esphome/components/partition/light_partition.cpp create mode 100644 esphome/components/partition/light_partition.h delete mode 100644 esphome/components/pca9685.py create mode 100644 esphome/components/pca9685/__init__.py create mode 100644 esphome/components/pca9685/output.py create mode 100644 esphome/components/pca9685/pca9685_output.cpp create mode 100644 esphome/components/pca9685/pca9685_output.h delete mode 100644 esphome/components/pcf8574.py create mode 100644 esphome/components/pcf8574/__init__.py create mode 100644 esphome/components/pcf8574/pcf8574.cpp create mode 100644 esphome/components/pcf8574/pcf8574.h create mode 100644 esphome/components/pmsx003/__init__.py create mode 100644 esphome/components/pmsx003/pmsx003.cpp create mode 100644 esphome/components/pmsx003/pmsx003.h create mode 100644 esphome/components/pmsx003/sensor.py delete mode 100644 esphome/components/pn532.py create mode 100644 esphome/components/pn532/__init__.py create mode 100644 esphome/components/pn532/binary_sensor.py create mode 100644 esphome/components/pn532/pn532.cpp create mode 100644 esphome/components/pn532/pn532.h delete mode 100644 esphome/components/power_supply.py create mode 100644 esphome/components/power_supply/__init__.py create mode 100644 esphome/components/power_supply/power_supply.cpp create mode 100644 esphome/components/power_supply/power_supply.h create mode 100644 esphome/components/pulse_counter/__init__.py create mode 100644 esphome/components/pulse_counter/pulse_counter_sensor.cpp create mode 100644 esphome/components/pulse_counter/pulse_counter_sensor.h create mode 100644 esphome/components/pulse_counter/sensor.py create mode 100644 esphome/components/pulse_width/__init__.py create mode 100644 esphome/components/pulse_width/pulse_width.cpp create mode 100644 esphome/components/pulse_width/pulse_width.h create mode 100644 esphome/components/pulse_width/sensor.py delete mode 100644 esphome/components/rdm6300.py create mode 100644 esphome/components/rdm6300/__init__.py create mode 100644 esphome/components/rdm6300/binary_sensor.py create mode 100644 esphome/components/rdm6300/rdm6300.cpp create mode 100644 esphome/components/rdm6300/rdm6300.h create mode 100644 esphome/components/remote_base/__init__.py create mode 100644 esphome/components/remote_base/jvc_protocol.cpp create mode 100644 esphome/components/remote_base/jvc_protocol.h create mode 100644 esphome/components/remote_base/lg_protocol.cpp create mode 100644 esphome/components/remote_base/lg_protocol.h create mode 100644 esphome/components/remote_base/nec_protocol.cpp create mode 100644 esphome/components/remote_base/nec_protocol.h create mode 100644 esphome/components/remote_base/panasonic_protocol.cpp create mode 100644 esphome/components/remote_base/panasonic_protocol.h create mode 100644 esphome/components/remote_base/raw_protocol.cpp create mode 100644 esphome/components/remote_base/raw_protocol.h create mode 100644 esphome/components/remote_base/rc5_protocol.cpp create mode 100644 esphome/components/remote_base/rc5_protocol.h create mode 100644 esphome/components/remote_base/rc_switch_protocol.cpp create mode 100644 esphome/components/remote_base/rc_switch_protocol.h create mode 100644 esphome/components/remote_base/remote_base.cpp create mode 100644 esphome/components/remote_base/remote_base.h create mode 100644 esphome/components/remote_base/samsung_protocol.cpp create mode 100644 esphome/components/remote_base/samsung_protocol.h create mode 100644 esphome/components/remote_base/sony_protocol.cpp create mode 100644 esphome/components/remote_base/sony_protocol.h delete mode 100644 esphome/components/remote_receiver.py create mode 100644 esphome/components/remote_receiver/__init__.py create mode 100644 esphome/components/remote_receiver/binary_sensor.py create mode 100644 esphome/components/remote_receiver/remote_receiver.h create mode 100644 esphome/components/remote_receiver/remote_receiver_esp32.cpp create mode 100644 esphome/components/remote_receiver/remote_receiver_esp8266.cpp delete mode 100644 esphome/components/remote_transmitter.py create mode 100644 esphome/components/remote_transmitter/__init__.py create mode 100644 esphome/components/remote_transmitter/remote_transmitter.cpp create mode 100644 esphome/components/remote_transmitter/remote_transmitter.h create mode 100644 esphome/components/remote_transmitter/remote_transmitter_esp32.cpp create mode 100644 esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp create mode 100644 esphome/components/remote_transmitter/switch.py create mode 100644 esphome/components/restart/__init__.py create mode 100644 esphome/components/restart/restart_switch.cpp create mode 100644 esphome/components/restart/restart_switch.h create mode 100644 esphome/components/restart/switch.py create mode 100644 esphome/components/rgb/__init__.py create mode 100644 esphome/components/rgb/light.py create mode 100644 esphome/components/rgb/rgb_light_output.h create mode 100644 esphome/components/rgbw/__init__.py create mode 100644 esphome/components/rgbw/light.py create mode 100644 esphome/components/rgbw/rgbw_light_output.h create mode 100644 esphome/components/rgbww/__init__.py create mode 100644 esphome/components/rgbww/light.py create mode 100644 esphome/components/rgbww/rgbww_light_output.h create mode 100644 esphome/components/rotary_encoder/__init__.py create mode 100644 esphome/components/rotary_encoder/rotary_encoder.cpp create mode 100644 esphome/components/rotary_encoder/rotary_encoder.h create mode 100644 esphome/components/rotary_encoder/sensor.py delete mode 100644 esphome/components/script.py create mode 100644 esphome/components/script/__init__.py create mode 100644 esphome/components/script/script.h create mode 100644 esphome/components/sds011/__init__.py create mode 100644 esphome/components/sds011/sds011.cpp create mode 100644 esphome/components/sds011/sds011.h create mode 100644 esphome/components/sds011/sensor.py delete mode 100644 esphome/components/sensor/adc.py delete mode 100644 esphome/components/sensor/apds9960.py create mode 100644 esphome/components/sensor/automation.cpp create mode 100644 esphome/components/sensor/automation.h delete mode 100644 esphome/components/sensor/bh1750.py delete mode 100644 esphome/components/sensor/ble_rssi.py delete mode 100644 esphome/components/sensor/bme280.py delete mode 100644 esphome/components/sensor/bme680.py delete mode 100644 esphome/components/sensor/bmp085.py delete mode 100644 esphome/components/sensor/bmp280.py delete mode 100644 esphome/components/sensor/cse7766.py delete mode 100644 esphome/components/sensor/custom.py delete mode 100644 esphome/components/sensor/dallas.py delete mode 100644 esphome/components/sensor/dht.py delete mode 100644 esphome/components/sensor/dht12.py delete mode 100644 esphome/components/sensor/duty_cycle.py delete mode 100644 esphome/components/sensor/esp32_hall.py create mode 100644 esphome/components/sensor/filter.cpp create mode 100644 esphome/components/sensor/filter.h delete mode 100644 esphome/components/sensor/hdc1080.py delete mode 100644 esphome/components/sensor/hlw8012.py delete mode 100644 esphome/components/sensor/hmc5883l.py delete mode 100644 esphome/components/sensor/homeassistant.py delete mode 100644 esphome/components/sensor/htu21d.py delete mode 100644 esphome/components/sensor/hx711.py delete mode 100644 esphome/components/sensor/ina219.py delete mode 100644 esphome/components/sensor/ina3221.py delete mode 100644 esphome/components/sensor/max31855.py delete mode 100644 esphome/components/sensor/max6675.py delete mode 100644 esphome/components/sensor/mhz19.py delete mode 100644 esphome/components/sensor/mpu6050.py delete mode 100644 esphome/components/sensor/mqtt_subscribe.py delete mode 100644 esphome/components/sensor/ms5611.py delete mode 100644 esphome/components/sensor/pmsx003.py delete mode 100644 esphome/components/sensor/pulse_counter.py delete mode 100644 esphome/components/sensor/rotary_encoder.py delete mode 100644 esphome/components/sensor/sds011.py create mode 100644 esphome/components/sensor/sensor.cpp create mode 100644 esphome/components/sensor/sensor.h delete mode 100644 esphome/components/sensor/sht3xd.py delete mode 100644 esphome/components/sensor/tcs34725.py delete mode 100644 esphome/components/sensor/template.py delete mode 100644 esphome/components/sensor/total_daily_energy.py delete mode 100644 esphome/components/sensor/tsl2561.py delete mode 100644 esphome/components/sensor/ultrasonic.py delete mode 100644 esphome/components/sensor/uptime.py delete mode 100644 esphome/components/sensor/wifi_signal.py delete mode 100644 esphome/components/sensor/xiaomi_miflora.py delete mode 100644 esphome/components/sensor/xiaomi_mijia.py delete mode 100644 esphome/components/servo.py create mode 100644 esphome/components/servo/__init__.py create mode 100644 esphome/components/servo/servo.cpp create mode 100644 esphome/components/servo/servo.h create mode 100644 esphome/components/sht3xd/__init__.py create mode 100644 esphome/components/sht3xd/sensor.py create mode 100644 esphome/components/sht3xd/sht3xd.cpp create mode 100644 esphome/components/sht3xd/sht3xd.h create mode 100644 esphome/components/shutdown/__init__.py create mode 100644 esphome/components/shutdown/shutdown_switch.cpp create mode 100644 esphome/components/shutdown/shutdown_switch.h create mode 100644 esphome/components/shutdown/switch.py create mode 100644 esphome/components/sntp/__init__.py create mode 100644 esphome/components/sntp/sntp_component.cpp create mode 100644 esphome/components/sntp/sntp_component.h create mode 100644 esphome/components/sntp/time.py create mode 100644 esphome/components/speed/__init__.py create mode 100644 esphome/components/speed/fan/__init__.py create mode 100644 esphome/components/speed/fan/speed_fan.cpp create mode 100644 esphome/components/speed/fan/speed_fan.h delete mode 100644 esphome/components/spi.py create mode 100644 esphome/components/spi/__init__.py create mode 100644 esphome/components/spi/spi.cpp create mode 100644 esphome/components/spi/spi.h create mode 100644 esphome/components/ssd1306_base/__init__.py create mode 100644 esphome/components/ssd1306_base/ssd1306_base.cpp create mode 100644 esphome/components/ssd1306_base/ssd1306_base.h create mode 100644 esphome/components/ssd1306_i2c/__init__.py create mode 100644 esphome/components/ssd1306_i2c/display.py create mode 100644 esphome/components/ssd1306_i2c/ssd1306_i2c.cpp create mode 100644 esphome/components/ssd1306_i2c/ssd1306_i2c.h create mode 100644 esphome/components/ssd1306_spi/__init__.py create mode 100644 esphome/components/ssd1306_spi/display.py create mode 100644 esphome/components/ssd1306_spi/ssd1306_spi.cpp create mode 100644 esphome/components/ssd1306_spi/ssd1306_spi.h create mode 100644 esphome/components/status/__init__.py create mode 100644 esphome/components/status/binary_sensor.py create mode 100644 esphome/components/status/status_binary_sensor.cpp create mode 100644 esphome/components/status/status_binary_sensor.h delete mode 100644 esphome/components/status_led.py create mode 100644 esphome/components/status_led/__init__.py create mode 100644 esphome/components/status_led/status_led.cpp create mode 100644 esphome/components/status_led/status_led.h delete mode 100644 esphome/components/stepper/a4988.py create mode 100644 esphome/components/stepper/stepper.cpp create mode 100644 esphome/components/stepper/stepper.h delete mode 100644 esphome/components/stepper/uln2003.py rename esphome/components/{substitutions.py => substitutions/__init__.py} (94%) create mode 100644 esphome/components/switch/automation.cpp create mode 100644 esphome/components/switch/automation.h delete mode 100644 esphome/components/switch/custom.py delete mode 100644 esphome/components/switch/gpio.py delete mode 100644 esphome/components/switch/output.py delete mode 100644 esphome/components/switch/remote_transmitter.py delete mode 100644 esphome/components/switch/restart.py delete mode 100644 esphome/components/switch/shutdown.py create mode 100644 esphome/components/switch/switch.cpp create mode 100644 esphome/components/switch/switch.h delete mode 100644 esphome/components/switch/template.py delete mode 100644 esphome/components/switch/uart.py create mode 100644 esphome/components/tcs34725/__init__.py create mode 100644 esphome/components/tcs34725/sensor.py create mode 100644 esphome/components/tcs34725/tcs34725.cpp create mode 100644 esphome/components/tcs34725/tcs34725.h create mode 100644 esphome/components/template/__init__.py create mode 100644 esphome/components/template/binary_sensor/__init__.py create mode 100644 esphome/components/template/binary_sensor/template_binary_sensor.cpp create mode 100644 esphome/components/template/binary_sensor/template_binary_sensor.h create mode 100644 esphome/components/template/cover/__init__.py create mode 100644 esphome/components/template/cover/template_cover.cpp create mode 100644 esphome/components/template/cover/template_cover.h create mode 100644 esphome/components/template/sensor/__init__.py create mode 100644 esphome/components/template/sensor/template_sensor.cpp create mode 100644 esphome/components/template/sensor/template_sensor.h create mode 100644 esphome/components/template/switch/__init__.py create mode 100644 esphome/components/template/switch/template_switch.cpp create mode 100644 esphome/components/template/switch/template_switch.h create mode 100644 esphome/components/template/text_sensor/__init__.py create mode 100644 esphome/components/template/text_sensor/template_text_sensor.cpp create mode 100644 esphome/components/template/text_sensor/template_text_sensor.h create mode 100644 esphome/components/text_sensor/automation.cpp create mode 100644 esphome/components/text_sensor/automation.h delete mode 100644 esphome/components/text_sensor/custom.py delete mode 100644 esphome/components/text_sensor/homeassistant.py delete mode 100644 esphome/components/text_sensor/mqtt_subscribe.py delete mode 100644 esphome/components/text_sensor/template.py create mode 100644 esphome/components/text_sensor/text_sensor.cpp create mode 100644 esphome/components/text_sensor/text_sensor.h delete mode 100644 esphome/components/text_sensor/version.py delete mode 100644 esphome/components/text_sensor/wifi_info.py create mode 100644 esphome/components/time/automation.cpp create mode 100644 esphome/components/time/automation.h delete mode 100644 esphome/components/time/homeassistant.py create mode 100644 esphome/components/time/real_time_clock.cpp create mode 100644 esphome/components/time/real_time_clock.h delete mode 100644 esphome/components/time/sntp.py create mode 100644 esphome/components/time_based/__init__.py create mode 100644 esphome/components/time_based/cover.py create mode 100644 esphome/components/time_based/time_based_cover.cpp create mode 100644 esphome/components/time_based/time_based_cover.h create mode 100644 esphome/components/total_daily_energy/__init__.py create mode 100644 esphome/components/total_daily_energy/sensor.py create mode 100644 esphome/components/total_daily_energy/total_daily_energy.cpp create mode 100644 esphome/components/total_daily_energy/total_daily_energy.h create mode 100644 esphome/components/tsl2561/__init__.py create mode 100644 esphome/components/tsl2561/sensor.py create mode 100644 esphome/components/tsl2561/tsl2561.cpp create mode 100644 esphome/components/tsl2561/tsl2561.h delete mode 100644 esphome/components/ttp229_lsf.py create mode 100644 esphome/components/ttp229_lsf/__init__.py create mode 100644 esphome/components/ttp229_lsf/binary_sensor.py create mode 100644 esphome/components/ttp229_lsf/ttp229.cpp create mode 100644 esphome/components/ttp229_lsf/ttp229.h delete mode 100644 esphome/components/uart.py create mode 100644 esphome/components/uart/__init__.py create mode 100644 esphome/components/uart/switch/__init__.py create mode 100644 esphome/components/uart/switch/uart_switch.cpp create mode 100644 esphome/components/uart/switch/uart_switch.h create mode 100644 esphome/components/uart/uart.cpp create mode 100644 esphome/components/uart/uart.h create mode 100644 esphome/components/uln2003/__init__.py create mode 100644 esphome/components/uln2003/stepper.py create mode 100644 esphome/components/uln2003/uln2003.cpp create mode 100644 esphome/components/uln2003/uln2003.h create mode 100644 esphome/components/ultrasonic/__init__.py create mode 100644 esphome/components/ultrasonic/sensor.py create mode 100644 esphome/components/ultrasonic/ultrasonic_sensor.cpp create mode 100644 esphome/components/ultrasonic/ultrasonic_sensor.h create mode 100644 esphome/components/uptime/__init__.py create mode 100644 esphome/components/uptime/sensor.py create mode 100644 esphome/components/uptime/uptime_sensor.cpp create mode 100644 esphome/components/uptime/uptime_sensor.h create mode 100644 esphome/components/version/__init__.py create mode 100644 esphome/components/version/text_sensor.py create mode 100644 esphome/components/version/version_text_sensor.cpp create mode 100644 esphome/components/version/version_text_sensor.h create mode 100644 esphome/components/waveshare_epaper/__init__.py create mode 100644 esphome/components/waveshare_epaper/display.py create mode 100644 esphome/components/waveshare_epaper/waveshare_epaper.cpp create mode 100644 esphome/components/waveshare_epaper/waveshare_epaper.h delete mode 100644 esphome/components/web_server.py create mode 100644 esphome/components/web_server/__init__.py create mode 100644 esphome/components/web_server/web_server.cpp create mode 100644 esphome/components/web_server/web_server.h delete mode 100644 esphome/components/wifi.py create mode 100644 esphome/components/wifi/__init__.py create mode 100644 esphome/components/wifi/wifi_component.cpp create mode 100644 esphome/components/wifi/wifi_component.h create mode 100644 esphome/components/wifi/wifi_component_esp32.cpp create mode 100644 esphome/components/wifi/wifi_component_esp8266.cpp create mode 100644 esphome/components/wifi_info/__init__.py create mode 100644 esphome/components/wifi_info/text_sensor.py create mode 100644 esphome/components/wifi_info/wifi_info_text_sensor.h create mode 100644 esphome/components/wifi_signal/__init__.py create mode 100644 esphome/components/wifi_signal/sensor.py create mode 100644 esphome/components/wifi_signal/wifi_signal_sensor.cpp create mode 100644 esphome/components/wifi_signal/wifi_signal_sensor.h create mode 100644 esphome/components/xiaomi_ble/__init__.py create mode 100644 esphome/components/xiaomi_ble/xiaomi_ble.cpp create mode 100644 esphome/components/xiaomi_ble/xiaomi_ble.h create mode 100644 esphome/components/xiaomi_miflora/__init__.py create mode 100644 esphome/components/xiaomi_miflora/sensor.py create mode 100644 esphome/components/xiaomi_miflora/xiaomi_miflora.cpp create mode 100644 esphome/components/xiaomi_miflora/xiaomi_miflora.h create mode 100644 esphome/components/xiaomi_mijia/__init__.py create mode 100644 esphome/components/xiaomi_mijia/sensor.py create mode 100644 esphome/components/xiaomi_mijia/xiaomi_mijia.cpp create mode 100644 esphome/components/xiaomi_mijia/xiaomi_mijia.h create mode 100644 esphome/core/application.cpp create mode 100644 esphome/core/application.h create mode 100644 esphome/core/automation.cpp create mode 100644 esphome/core/automation.h create mode 100644 esphome/core/automation.tcc create mode 100644 esphome/core/component.cpp create mode 100644 esphome/core/component.h create mode 100644 esphome/core/controller.cpp create mode 100644 esphome/core/controller.h create mode 100644 esphome/core/defines.h create mode 100644 esphome/core/esphal.cpp create mode 100644 esphome/core/esphal.h create mode 100644 esphome/core/helpers.cpp create mode 100644 esphome/core/helpers.h create mode 100644 esphome/core/log.cpp create mode 100644 esphome/core/log.h create mode 100644 esphome/core/optional.h create mode 100644 esphome/core/preferences.cpp create mode 100644 esphome/core/preferences.h create mode 100644 esphome/core/util.cpp create mode 100644 esphome/core/util.h delete mode 100755 esphome/espota.py create mode 100644 platformio.ini create mode 100644 requirements_test.txt create mode 100644 script/.neopixelbus.patch create mode 100755 script/ci-custom.py create mode 100755 script/ci-suggest-changes create mode 100755 script/clang-format.py create mode 100755 script/clang-tidy.py create mode 100755 script/lint-cpp create mode 100755 script/lint-python create mode 100755 script/setup create mode 100755 script/test create mode 100644 tests/livingroom32.cpp create mode 100644 tests/livingroom8266.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..a7c337f80 --- /dev/null +++ b/.clang-format @@ -0,0 +1,137 @@ +Language: Cpp +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: DontAlign +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: MultiLine +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 120 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: true +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^' + Priority: 2 + - Regex: '^<.*\.h>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 2 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 2000 +PointerAlignment: Right +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + CanonicalDelimiter: '' + BasedOnStyle: google +ReflowComments: true +SortIncludes: false +SortUsingDeclarations: false +SpaceAfterCStyleCast: true +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInContainerLiterals: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 2 +UseTab: Never diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 000000000..5e486e6a0 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,127 @@ +--- +Checks: >- + *, + -abseil-*, + -android-*, + -boost-*, + -bugprone-macro-parentheses, + -cert-dcl50-cpp, + -cert-err58-cpp, + -clang-analyzer-core.CallAndMessage, + -clang-analyzer-osx.*, + -clang-analyzer-security.*, + -cppcoreguidelines-avoid-goto, + -cppcoreguidelines-c-copy-assignment-signature, + -cppcoreguidelines-owning-memory, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-pro-bounds-constant-array-index, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -cppcoreguidelines-pro-type-const-cast, + -cppcoreguidelines-pro-type-cstyle-cast, + -cppcoreguidelines-pro-type-member-init, + -cppcoreguidelines-pro-type-reinterpret-cast, + -cppcoreguidelines-pro-type-static-cast-downcast, + -cppcoreguidelines-pro-type-union-access, + -cppcoreguidelines-pro-type-vararg, + -cppcoreguidelines-special-member-functions, + -fuchsia-*, + -fuchsia-default-arguments, + -fuchsia-multiple-inheritance, + -fuchsia-overloaded-operator, + -fuchsia-statically-constructed-objects, + -google-build-using-namespace, + -google-explicit-constructor, + -google-readability-braces-around-statements, + -google-readability-casting, + -google-readability-todo, + -google-runtime-int, + -google-runtime-references, + -hicpp-*, + -llvm-header-guard, + -llvm-include-order, + -misc-unconventional-assign-operator, + -misc-unused-parameters, + -modernize-deprecated-headers, + -modernize-pass-by-value, + -modernize-pass-by-value, + -modernize-return-braced-init-list, + -modernize-use-auto, + -modernize-use-default-member-init, + -modernize-use-equals-default, + -mpi-*, + -objc-*, + -performance-unnecessary-value-param, + -readability-braces-around-statements, + -readability-else-after-return, + -readability-implicit-bool-conversion, + -readability-named-parameter, + -readability-redundant-member-init, + -warnings-as-errors, + -zircon-* +WarningsAsErrors: '*' +HeaderFilterRegex: '^.*/src/esphome/.*' +AnalyzeTemporaryDtors: false +FormatStyle: google +CheckOptions: + - key: google-readability-braces-around-statements.ShortStatementLines + value: '1' + - key: google-readability-function-size.StatementThreshold + value: '800' + - key: google-readability-namespace-comments.ShortNamespaceLines + value: '10' + - key: google-readability-namespace-comments.SpacesBeforeComments + value: '2' + - key: modernize-loop-convert.MaxCopySize + value: '16' + - key: modernize-loop-convert.MinConfidence + value: reasonable + - key: modernize-loop-convert.NamingStyle + value: CamelCase + - key: modernize-pass-by-value.IncludeStyle + value: llvm + - key: modernize-replace-auto-ptr.IncludeStyle + value: llvm + - key: modernize-use-nullptr.NullMacros + value: 'NULL' + - key: readability-identifier-naming.LocalVariableCase + value: 'lower_case' + - key: readability-identifier-naming.ClassCase + value: 'CamelCase' + - key: readability-identifier-naming.StructCase + value: 'CamelCase' + - key: readability-identifier-naming.EnumCase + value: 'CamelCase' + - key: readability-identifier-naming.EnumConstantCase + value: 'UPPER_CASE' + - key: readability-identifier-naming.StaticConstantCase + value: 'UPPER_CASE' + - key: readability-identifier-naming.StaticVariableCase + value: 'UPPER_CASE' + - key: readability-identifier-naming.GlobalConstantCase + value: 'UPPER_CASE' + - key: readability-identifier-naming.ParameterCase + value: 'lower_case' + - key: readability-identifier-naming.PrivateMemberPrefix + value: 'NO_PRIVATE_MEMBERS_ALWAYS_USE_PROTECTED' + - key: readability-identifier-naming.PrivateMethodPrefix + value: 'NO_PRIVATE_METHODS_ALWAYS_USE_PROTECTED' + - key: readability-identifier-naming.ClassMemberCase + value: 'lower_case' + - key: readability-identifier-naming.ClassMemberCase + value: 'lower_case' + - key: readability-identifier-naming.ProtectedMemberCase + value: 'lower_case' + - key: readability-identifier-naming.ProtectedMemberSuffix + value: '_' + - key: readability-identifier-naming.FunctionCase + value: 'lower_case' + - key: readability-identifier-naming.ClassMethodCase + value: 'lower_case' + - key: readability-identifier-naming.ProtectedMethodCase + value: 'lower_case' + - key: readability-identifier-naming.ProtectedMethodSuffix + value: '_' + - key: readability-identifier-naming.VirtualMethodCase + value: 'lower_case' + - key: readability-identifier-naming.VirtualMethodSuffix + value: '' diff --git a/.gitignore b/.gitignore index c8a36dfd3..6ca9361b1 100644 --- a/.gitignore +++ b/.gitignore @@ -25,12 +25,6 @@ wheels/ *.egg MANIFEST -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - # Installer logs pip-log.txt pip-delete-this-directory.txt @@ -51,36 +45,9 @@ coverage.xml *.mo *.pot -# Django stuff: -*.log -local_settings.py -db.sqlite3 - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - # pyenv .python-version -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - # Environments .env .venv @@ -90,19 +57,46 @@ ENV/ env.bak/ venv.bak/ -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - # mypy .mypy_cache/ +.pioenvs +.piolibdeps +.vscode +CMakeListsPrivate.txt +CMakeLists.txt + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/dynamic.xml + +# CMake +cmake-build-debug/ +cmake-build-release/ + +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +/*.cbp + +.clang_complete +.gcc-flags.json + config/ tests/build/ tests/.esphome/ +/.temp-clang-tidy.cpp diff --git a/.travis.yml b/.travis.yml index 4b59e2581..b5642478f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,10 @@ sudo: false language: python - +install: script/setup cache: directories: - "~/.platformio" + - "$TRAVIS_BUILD_DIR/.piolibdeps" - "$TRAVIS_BUILD_DIR/tests/build/test1/.piolibdeps" - "$TRAVIS_BUILD_DIR/tests/build/test2/.piolibdeps" - "$TRAVIS_BUILD_DIR/tests/build/test3/.piolibdeps" @@ -13,26 +14,43 @@ matrix: include: - python: "2.7" env: TARGET=Lint2.7 - install: pip install -e . && pip install flake8==3.6.0 pylint==1.9.4 pillow script: + - script/ci-custom.py - flake8 esphome - pylint esphome - python: "3.5.3" env: TARGET=Lint3.5 - install: pip install -U https://github.com/platformio/platformio-core/archive/develop.zip && pip install -e . && pip install flake8==3.6.0 pylint==2.3.0 pillow script: + - script/ci-custom.py - flake8 esphome - pylint esphome - python: "2.7" env: TARGET=Test2.7 - install: pip install -e . && pip install flake8==3.6.0 pylint==1.9.4 pillow script: - esphome tests/test1.yaml compile - esphome tests/test2.yaml compile - esphome tests/test3.yaml compile - #- python: "3.5.3" - # env: TARGET=Test3.5 - # install: pip install -U https://github.com/platformio/platformio-core/archive/develop.zip && pip install -e . && pip install flake8==3.6.0 pylint==2.3.0 pillow - # script: - # - esphome tests/test1.yaml compile - # - esphome tests/test2.yaml compile + - env: TARGET=Cpp-Lint + dist: trusty + sudo: required + addons: + apt: + sources: + - ubuntu-toolchain-r-test + - llvm-toolchain-trusty-7 + packages: + - clang-tidy-7 + - clang-format-7 + before_script: + - pio init --ide atom + - | + if ! patch -R -p0 -s -f --dry-run ")); + + request->send(stream); +} + +#ifdef USE_SENSOR +void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { + this->events_.send(this->sensor_json(obj, state).c_str(), "state"); +} +void WebServer::handle_sensor_request(AsyncWebServerRequest *request, UrlMatch match) { + for (sensor::Sensor *obj : App.get_sensors()) { + if (obj->is_internal()) + continue; + if (obj->get_object_id() != match.id) + continue; + std::string data = this->sensor_json(obj, obj->state); + request->send(200, "text/json", data.c_str()); + return; + } + request->send(404); +} +std::string WebServer::sensor_json(sensor::Sensor *obj, float value) { + return json::build_json([obj, value](JsonObject &root) { + root["id"] = "sensor-" + obj->get_object_id(); + std::string state = value_accuracy_to_string(value, obj->get_accuracy_decimals()); + if (!obj->get_unit_of_measurement().empty()) + state += " " + obj->get_unit_of_measurement(); + root["state"] = state; + root["value"] = value; + }); +} +#endif + +#ifdef USE_TEXT_SENSOR +void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, std::string state) { + this->events_.send(this->text_sensor_json(obj, state).c_str(), "state"); +} +void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, UrlMatch match) { + for (text_sensor::TextSensor *obj : App.get_text_sensors()) { + if (obj->is_internal()) + continue; + if (obj->get_object_id() != match.id) + continue; + std::string data = this->text_sensor_json(obj, obj->state); + request->send(200, "text/json", data.c_str()); + return; + } + request->send(404); +} +std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std::string &value) { + return json::build_json([obj, value](JsonObject &root) { + root["id"] = "text_sensor-" + obj->get_object_id(); + root["state"] = value; + root["value"] = value; + }); +} +#endif + +#ifdef USE_SWITCH +void WebServer::on_switch_update(switch_::Switch *obj, bool state) { + this->events_.send(this->switch_json(obj, state).c_str(), "state"); +} +std::string WebServer::switch_json(switch_::Switch *obj, bool value) { + return json::build_json([obj, value](JsonObject &root) { + root["id"] = "switch-" + obj->get_object_id(); + root["state"] = value ? "ON" : "OFF"; + root["value"] = value; + }); +} +void WebServer::handle_switch_request(AsyncWebServerRequest *request, UrlMatch match) { + for (switch_::Switch *obj : App.get_switches()) { + if (obj->is_internal()) + continue; + if (obj->get_object_id() != match.id) + continue; + + if (request->method() == HTTP_GET) { + std::string data = this->switch_json(obj, obj->state); + request->send(200, "text/json", data.c_str()); + } else if (match.method == "toggle") { + this->defer([obj]() { obj->toggle(); }); + request->send(200); + } else if (match.method == "turn_on") { + this->defer([obj]() { obj->turn_on(); }); + request->send(200); + } else if (match.method == "turn_off") { + this->defer([obj]() { obj->turn_off(); }); + request->send(200); + } else { + request->send(404); + } + return; + } + request->send(404); +} +#endif + +#ifdef USE_BINARY_SENSOR +void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) { + if (obj->is_internal()) + return; + this->events_.send(this->binary_sensor_json(obj, state).c_str(), "state"); +} +std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value) { + return json::build_json([obj, value](JsonObject &root) { + root["id"] = "binary_sensor-" + obj->get_object_id(); + root["state"] = value ? "ON" : "OFF"; + root["value"] = value; + }); +} +void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, UrlMatch match) { + for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) { + if (obj->is_internal()) + continue; + if (obj->get_object_id() != match.id) + continue; + std::string data = this->binary_sensor_json(obj, obj->state); + request->send(200, "text/json", data.c_str()); + return; + } + request->send(404); +} +#endif + +#ifdef USE_FAN +void WebServer::on_fan_update(fan::FanState *obj) { + if (obj->is_internal()) + return; + this->events_.send(this->fan_json(obj).c_str(), "state"); +} +std::string WebServer::fan_json(fan::FanState *obj) { + return json::build_json([obj](JsonObject &root) { + root["id"] = "fan-" + obj->get_object_id(); + root["state"] = obj->state ? "ON" : "OFF"; + root["value"] = obj->state; + if (obj->get_traits().supports_speed()) { + switch (obj->speed) { + case fan::FAN_SPEED_LOW: + root["speed"] = "low"; + break; + case fan::FAN_SPEED_MEDIUM: + root["speed"] = "medium"; + break; + case fan::FAN_SPEED_HIGH: + root["speed"] = "high"; + break; + } + } + if (obj->get_traits().supports_oscillation()) + root["oscillation"] = obj->oscillating; + }); +} +void WebServer::handle_fan_request(AsyncWebServerRequest *request, UrlMatch match) { + for (fan::FanState *obj : App.get_fans()) { + if (obj->is_internal()) + continue; + if (obj->get_object_id() != match.id) + continue; + + if (request->method() == HTTP_GET) { + std::string data = this->fan_json(obj); + request->send(200, "text/json", data.c_str()); + } else if (match.method == "toggle") { + this->defer([obj]() { obj->toggle().perform(); }); + request->send(200); + } else if (match.method == "turn_on") { + auto call = obj->turn_on(); + if (request->hasParam("speed")) { + String speed = request->getParam("speed")->value(); + call.set_speed(speed.c_str()); + } + if (request->hasParam("oscillation")) { + String speed = request->getParam("oscillation")->value(); + auto val = parse_on_off(speed.c_str()); + switch (val) { + case PARSE_ON: + call.set_oscillating(true); + break; + case PARSE_OFF: + call.set_oscillating(false); + break; + case PARSE_TOGGLE: + call.set_oscillating(!obj->oscillating); + break; + case PARSE_NONE: + request->send(404); + return; + } + } + this->defer([call]() { call.perform(); }); + request->send(200); + } else if (match.method == "turn_off") { + this->defer([obj]() { obj->turn_off().perform(); }); + request->send(200); + } else { + request->send(404); + } + return; + } + request->send(404); +} +#endif + +#ifdef USE_LIGHT +void WebServer::on_light_update(light::LightState *obj) { + if (obj->is_internal()) + return; + this->events_.send(this->light_json(obj).c_str(), "state"); +} +void WebServer::handle_light_request(AsyncWebServerRequest *request, UrlMatch match) { + for (light::LightState *obj : App.get_lights()) { + if (obj->is_internal()) + continue; + if (obj->get_object_id() != match.id) + continue; + + if (request->method() == HTTP_GET) { + std::string data = this->light_json(obj); + request->send(200, "text/json", data.c_str()); + } else if (match.method == "toggle") { + this->defer([obj]() { obj->toggle().perform(); }); + request->send(200); + } else if (match.method == "turn_on") { + auto call = obj->turn_on(); + if (request->hasParam("brightness")) + call.set_brightness(request->getParam("brightness")->value().toFloat() / 255.0f); + if (request->hasParam("r")) + call.set_red(request->getParam("r")->value().toFloat() / 255.0f); + if (request->hasParam("g")) + call.set_green(request->getParam("g")->value().toFloat() / 255.0f); + if (request->hasParam("b")) + call.set_blue(request->getParam("b")->value().toFloat() / 255.0f); + if (request->hasParam("white_value")) + call.set_white(request->getParam("white_value")->value().toFloat() / 255.0f); + if (request->hasParam("color_temp")) + call.set_color_temperature(request->getParam("color_temp")->value().toFloat()); + + if (request->hasParam("flash")) + call.set_flash_length((uint32_t) request->getParam("flash")->value().toFloat() * 1000); + + if (request->hasParam("transition")) + call.set_transition_length((uint32_t) request->getParam("transition")->value().toFloat() * 1000); + + if (request->hasParam("effect")) { + const char *effect = request->getParam("effect")->value().c_str(); + call.set_effect(effect); + } + + this->defer([call]() mutable { call.perform(); }); + request->send(200); + } else if (match.method == "turn_off") { + auto call = obj->turn_off(); + if (request->hasParam("transition")) { + auto length = (uint32_t) request->getParam("transition")->value().toFloat() * 1000; + call.set_transition_length(length); + } + this->defer([call]() mutable { call.perform(); }); + request->send(200); + } else { + request->send(404); + } + return; + } + request->send(404); +} +std::string WebServer::light_json(light::LightState *obj) { + return json::build_json([obj](JsonObject &root) { + root["id"] = "light-" + obj->get_object_id(); + root["state"] = obj->remote_values.is_on() ? "ON" : "OFF"; + obj->dump_json(root); + }); +} +#endif + +bool WebServer::canHandle(AsyncWebServerRequest *request) { + if (request->url() == "/") + return true; + + if (request->url() == "/update" && request->method() == HTTP_POST) + return true; + + UrlMatch match = match_url(request->url().c_str(), true); + if (!match.valid) + return false; +#ifdef USE_SENSOR + if (request->method() == HTTP_GET && match.domain == "sensor") + return true; +#endif + +#ifdef USE_SWITCH + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "switch") + return true; +#endif + +#ifdef USE_BINARY_SENSOR + if (request->method() == HTTP_GET && match.domain == "binary_sensor") + return true; +#endif + +#ifdef USE_FAN + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "fan") + return true; +#endif + +#ifdef USE_LIGHT + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "light") + return true; +#endif + +#ifdef USE_TEXT_SENSOR + if (request->method() == HTTP_GET && match.domain == "text_sensor") + return true; +#endif + + return false; +} +void WebServer::handleRequest(AsyncWebServerRequest *request) { + if (request->url() == "/") { + this->handle_index_request(request); + return; + } + + if (request->url() == "/update") { + this->handle_update_request(request); + return; + } + + UrlMatch match = match_url(request->url().c_str()); +#ifdef USE_SENSOR + if (match.domain == "sensor") { + this->handle_sensor_request(request, match); + return; + } +#endif + +#ifdef USE_SWITCH + if (match.domain == "switch") { + this->handle_switch_request(request, match); + return; + } +#endif + +#ifdef USE_BINARY_SENSOR + if (match.domain == "binary_sensor") { + this->handle_binary_sensor_request(request, match); + return; + } +#endif + +#ifdef USE_FAN + if (match.domain == "fan") { + this->handle_fan_request(request, match); + return; + } +#endif + +#ifdef USE_LIGHT + if (match.domain == "light") { + this->handle_light_request(request, match); + return; + } +#endif + +#ifdef USE_TEXT_SENSOR + if (match.domain == "text_sensor") { + this->handle_text_sensor_request(request, match); + return; + } +#endif +} + +bool WebServer::isRequestHandlerTrivial() { return false; } + +} // namespace web_server +} // namespace esphome diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h new file mode 100644 index 000000000..72db3e36c --- /dev/null +++ b/esphome/components/web_server/web_server.h @@ -0,0 +1,142 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/controller.h" + +#include +#include + +namespace esphome { +namespace web_server { + +/// Internal helper struct that is used to parse incoming URLs +struct UrlMatch { + std::string domain; ///< The domain of the component, for example "sensor" + std::string id; ///< The id of the device that's being accessed, for example "living_room_fan" + std::string method; ///< The method that's being called, for example "turn_on" + bool valid; ///< Whether this match is valid +}; + +/** This class allows users to create a web server with their ESP nodes. + * + * Behind the scenes it's using AsyncWebServer to set up the server. It exposes 3 things: + * an index page under '/' that's used to show a simple web interface (the css/js is hosted + * by esphome.io by default), an event source under '/events' that automatically sends + * all state updates in real time + the debug log. Lastly, there's an REST API available + * under the '/light/...', '/sensor/...', ... URLs. A full documentation for this API + * can be found under https://esphome.io/web-api/index.html. + */ +class WebServer : public Controller, public Component, public AsyncWebHandler { + public: + /// Initialize the web server with the specified port + explicit WebServer(uint16_t port); + + /** Set the URL to the CSS that's sent to each client. Defaults to + * https://esphome.io/_static/webserver-v1.min.css + * + * @param css_url The url to the web server stylesheet. + */ + void set_css_url(const char *css_url); + + /** Set the URL to the script that's embedded in the index page. Defaults to + * https://esphome.io/_static/webserver-v1.min.js + * + * @param js_url The url to the web server script. + */ + void set_js_url(const char *js_url); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + /// Setup the internal web server and register handlers. + void setup() override; + + void dump_config() override; + + /// MQTT setup priority. + float get_setup_priority() const override; + + /// Handle an index request under '/'. + void handle_index_request(AsyncWebServerRequest *request); + + void handle_update_request(AsyncWebServerRequest *request); + +#ifdef USE_SENSOR + void on_sensor_update(sensor::Sensor *obj, float state) override; + /// Handle a sensor request under '/sensor/'. + void handle_sensor_request(AsyncWebServerRequest *request, UrlMatch match); + + /// Dump the sensor state with its value as a JSON string. + std::string sensor_json(sensor::Sensor *obj, float value); +#endif + +#ifdef USE_SWITCH + void on_switch_update(switch_::Switch *obj, bool state) override; + + /// Handle a switch request under '/switch//'. + void handle_switch_request(AsyncWebServerRequest *request, UrlMatch match); + + /// Dump the switch state with its value as a JSON string. + std::string switch_json(switch_::Switch *obj, bool value); +#endif + +#ifdef USE_BINARY_SENSOR + void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override; + + /// Handle a binary sensor request under '/binary_sensor/'. + void handle_binary_sensor_request(AsyncWebServerRequest *request, UrlMatch match); + + /// Dump the binary sensor state with its value as a JSON string. + std::string binary_sensor_json(binary_sensor::BinarySensor *obj, bool value); +#endif + +#ifdef USE_FAN + void on_fan_update(fan::FanState *obj) override; + + /// Handle a fan request under '/fan//'. + void handle_fan_request(AsyncWebServerRequest *request, UrlMatch match); + + /// Dump the fan state as a JSON string. + std::string fan_json(fan::FanState *obj); +#endif + +#ifdef USE_LIGHT + void on_light_update(light::LightState *obj) override; + + /// Handle a light request under '/light//'. + void handle_light_request(AsyncWebServerRequest *request, UrlMatch match); + + /// Dump the light state as a JSON string. + std::string light_json(light::LightState *obj); +#endif + +#ifdef USE_TEXT_SENSOR + void on_text_sensor_update(text_sensor::TextSensor *obj, std::string state) override; + + /// Handle a text sensor request under '/text_sensor/'. + void handle_text_sensor_request(AsyncWebServerRequest *request, UrlMatch match); + + /// Dump the text sensor state with its value as a JSON string. + std::string text_sensor_json(text_sensor::TextSensor *obj, const std::string &value); +#endif + + /// Override the web handler's canHandle method. + bool canHandle(AsyncWebServerRequest *request) override; + /// Override the web handler's handleRequest method. + void handleRequest(AsyncWebServerRequest *request) override; + void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, + bool final) override; + /// This web handle is not trivial. + bool isRequestHandlerTrivial() override; + + protected: + uint16_t port_; + AsyncWebServer *server_; + AsyncEventSource events_{"/events"}; + const char *css_url_{nullptr}; + const char *js_url_{nullptr}; + uint32_t last_ota_progress_{0}; + uint32_t ota_read_length_{0}; +}; + +} // namespace web_server +} // namespace esphome diff --git a/esphome/components/wifi.py b/esphome/components/wifi.py deleted file mode 100644 index e9932d06e..000000000 --- a/esphome/components/wifi.py +++ /dev/null @@ -1,203 +0,0 @@ -import voluptuous as vol - -from esphome.automation import CONDITION_REGISTRY, Condition -import esphome.config_validation as cv -from esphome.const import CONF_AP, CONF_BSSID, CONF_CHANNEL, CONF_DNS1, CONF_DNS2, CONF_DOMAIN, \ - CONF_FAST_CONNECT, CONF_GATEWAY, CONF_HIDDEN, CONF_ID, CONF_MANUAL_IP, CONF_NETWORKS, \ - CONF_PASSWORD, CONF_POWER_SAVE_MODE, CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, \ - CONF_SUBNET, CONF_USE_ADDRESS -from esphome.core import CORE, HexInt -from esphome.cpp_generator import Pvariable, StructInitializer, add, variable -from esphome.cpp_types import App, Component, esphome_ns, global_ns - -IPAddress = global_ns.class_('IPAddress') -ManualIP = esphome_ns.struct('ManualIP') -WiFiComponent = esphome_ns.class_('WiFiComponent', Component) -WiFiAP = esphome_ns.struct('WiFiAP') - -WiFiPowerSaveMode = esphome_ns.enum('WiFiPowerSaveMode') -WIFI_POWER_SAVE_MODES = { - 'NONE': WiFiPowerSaveMode.WIFI_POWER_SAVE_NONE, - 'LIGHT': WiFiPowerSaveMode.WIFI_POWER_SAVE_LIGHT, - 'HIGH': WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH, -} -WiFiConnectedCondition = esphome_ns.class_('WiFiConnectedCondition', Condition) - - -def validate_password(value): - value = cv.string_strict(value) - if not value: - return value - if len(value) < 8: - raise vol.Invalid(u"WPA password must be at least 8 characters long") - if len(value) > 64: - raise vol.Invalid(u"WPA password must be at most 64 characters long") - return value - - -def validate_channel(value): - value = cv.positive_int(value) - if value < 1: - raise vol.Invalid("Minimum WiFi channel is 1") - if value > 14: - raise vol.Invalid("Maximum WiFi channel is 14") - return value - - -AP_MANUAL_IP_SCHEMA = cv.Schema({ - vol.Required(CONF_STATIC_IP): cv.ipv4, - vol.Required(CONF_GATEWAY): cv.ipv4, - vol.Required(CONF_SUBNET): cv.ipv4, -}) - -STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend({ - vol.Optional(CONF_DNS1, default="1.1.1.1"): cv.ipv4, - vol.Optional(CONF_DNS2, default="1.0.0.1"): cv.ipv4, -}) - -WIFI_NETWORK_BASE = cv.Schema({ - cv.GenerateID(): cv.declare_variable_id(WiFiAP), - vol.Optional(CONF_SSID): cv.ssid, - vol.Optional(CONF_PASSWORD): validate_password, - vol.Optional(CONF_CHANNEL): validate_channel, - vol.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, -}) - -WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend({ - -}) - -WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend({ - vol.Optional(CONF_BSSID): cv.mac_address, - vol.Optional(CONF_HIDDEN): cv.boolean, -}) - - -def validate(config): - if CONF_PASSWORD in config and CONF_SSID not in config: - raise vol.Invalid("Cannot have WiFi password without SSID!") - - if CONF_SSID in config: - network = {CONF_SSID: config.pop(CONF_SSID)} - if CONF_PASSWORD in config: - network[CONF_PASSWORD] = config.pop(CONF_PASSWORD) - if CONF_NETWORKS in config: - raise vol.Invalid("You cannot use the 'ssid:' option together with 'networks:'. Please " - "copy your network into the 'networks:' key") - config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network) - - if (CONF_NETWORKS not in config) and (CONF_AP not in config): - raise vol.Invalid("Please specify at least an SSID or an Access Point " - "to create.") - - if config.get(CONF_FAST_CONNECT, False): - networks = config.get(CONF_NETWORKS, []) - if not networks: - raise vol.Invalid("At least one network required for fast_connect!") - if len(networks) != 1: - raise vol.Invalid("Fast connect can only be used with one network!") - - if CONF_USE_ADDRESS not in config: - if CONF_MANUAL_IP in config: - use_address = str(config[CONF_MANUAL_IP][CONF_STATIC_IP]) - else: - use_address = CORE.name + config[CONF_DOMAIN] - config[CONF_USE_ADDRESS] = use_address - - return config - - -CONFIG_SCHEMA = vol.All(cv.Schema({ - cv.GenerateID(): cv.declare_variable_id(WiFiComponent), - vol.Optional(CONF_NETWORKS): cv.ensure_list(WIFI_NETWORK_STA), - - vol.Optional(CONF_SSID): cv.ssid, - vol.Optional(CONF_PASSWORD): validate_password, - vol.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, - - vol.Optional(CONF_AP): WIFI_NETWORK_AP, - vol.Optional(CONF_DOMAIN, default='.local'): cv.domain_name, - vol.Optional(CONF_REBOOT_TIMEOUT): cv.positive_time_period_milliseconds, - vol.Optional(CONF_POWER_SAVE_MODE): cv.one_of(*WIFI_POWER_SAVE_MODES, upper=True), - vol.Optional(CONF_FAST_CONNECT): cv.boolean, - vol.Optional(CONF_USE_ADDRESS): cv.string_strict, - - vol.Optional('hostname'): cv.invalid("The hostname option has been removed in 1.11.0"), -}), validate) - - -def safe_ip(ip): - if ip is None: - return IPAddress(0, 0, 0, 0) - return IPAddress(*ip.args) - - -def manual_ip(config): - if config is None: - return None - return StructInitializer( - ManualIP, - ('static_ip', safe_ip(config[CONF_STATIC_IP])), - ('gateway', safe_ip(config[CONF_GATEWAY])), - ('subnet', safe_ip(config[CONF_SUBNET])), - ('dns1', safe_ip(config.get(CONF_DNS1))), - ('dns2', safe_ip(config.get(CONF_DNS2))), - ) - - -def wifi_network(config, static_ip): - ap = variable(config[CONF_ID], WiFiAP()) - if CONF_SSID in config: - add(ap.set_ssid(config[CONF_SSID])) - if CONF_PASSWORD in config: - add(ap.set_password(config[CONF_PASSWORD])) - if CONF_BSSID in config: - add(ap.set_bssid([HexInt(i) for i in config[CONF_BSSID].parts])) - if CONF_HIDDEN in config: - add(ap.set_hidden(config[CONF_HIDDEN])) - if CONF_CHANNEL in config: - add(ap.set_channel(config[CONF_CHANNEL])) - if static_ip is not None: - add(ap.set_manual_ip(manual_ip(static_ip))) - - return ap - - -def to_code(config): - rhs = App.init_wifi() - wifi = Pvariable(config[CONF_ID], rhs) - add(wifi.set_use_address(config[CONF_USE_ADDRESS])) - - for network in config.get(CONF_NETWORKS, []): - add(wifi.add_sta(wifi_network(network, config.get(CONF_MANUAL_IP)))) - - if CONF_AP in config: - add(wifi.set_ap(wifi_network(config[CONF_AP], config.get(CONF_MANUAL_IP)))) - - if CONF_REBOOT_TIMEOUT in config: - add(wifi.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) - - if CONF_POWER_SAVE_MODE in config: - add(wifi.set_power_save_mode(WIFI_POWER_SAVE_MODES[config[CONF_POWER_SAVE_MODE]])) - - if CONF_FAST_CONNECT in config: - add(wifi.set_fast_connect(config[CONF_FAST_CONNECT])) - - -def lib_deps(config): - if CORE.is_esp8266: - return 'ESP8266WiFi' - if CORE.is_esp32: - return None - raise NotImplementedError - - -CONF_WIFI_CONNECTED = 'wifi.connected' -WIFI_CONNECTED_CONDITION_SCHEMA = cv.Schema({}) - - -@CONDITION_REGISTRY.register(CONF_WIFI_CONNECTED, WIFI_CONNECTED_CONDITION_SCHEMA) -def wifi_connected_to_code(config, condition_id, template_arg, args): - rhs = WiFiConnectedCondition.new(template_arg) - type = WiFiConnectedCondition.template(template_arg) - yield Pvariable(condition_id, rhs, type=type) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py new file mode 100644 index 000000000..8be00a6c2 --- /dev/null +++ b/esphome/components/wifi/__init__.py @@ -0,0 +1,194 @@ +from esphome.automation import CONDITION_REGISTRY, Condition +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_AP, CONF_BSSID, CONF_CHANNEL, CONF_DNS1, CONF_DNS2, CONF_DOMAIN, \ + CONF_FAST_CONNECT, CONF_GATEWAY, CONF_HIDDEN, CONF_ID, CONF_MANUAL_IP, CONF_NETWORKS, \ + CONF_PASSWORD, CONF_POWER_SAVE_MODE, CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, \ + CONF_SUBNET, CONF_USE_ADDRESS +from esphome.core import CORE, HexInt, coroutine_with_priority + +wifi_ns = cg.esphome_ns.namespace('wifi') +IPAddress = cg.global_ns.class_('IPAddress') +ManualIP = wifi_ns.struct('ManualIP') +WiFiComponent = wifi_ns.class_('WiFiComponent', cg.Component) +WiFiAP = wifi_ns.struct('WiFiAP') + +WiFiPowerSaveMode = wifi_ns.enum('WiFiPowerSaveMode') +WIFI_POWER_SAVE_MODES = { + 'NONE': WiFiPowerSaveMode.WIFI_POWER_SAVE_NONE, + 'LIGHT': WiFiPowerSaveMode.WIFI_POWER_SAVE_LIGHT, + 'HIGH': WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH, +} +WiFiConnectedCondition = wifi_ns.class_('WiFiConnectedCondition', Condition) + + +def validate_password(value): + value = cv.string_strict(value) + if not value: + return value + if len(value) < 8: + raise cv.Invalid(u"WPA password must be at least 8 characters long") + if len(value) > 64: + raise cv.Invalid(u"WPA password must be at most 64 characters long") + return value + + +def validate_channel(value): + value = cv.positive_int(value) + if value < 1: + raise cv.Invalid("Minimum WiFi channel is 1") + if value > 14: + raise cv.Invalid("Maximum WiFi channel is 14") + return value + + +AP_MANUAL_IP_SCHEMA = cv.Schema({ + cv.Required(CONF_STATIC_IP): cv.ipv4, + cv.Required(CONF_GATEWAY): cv.ipv4, + cv.Required(CONF_SUBNET): cv.ipv4, +}) + +STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend({ + cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4, + cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4, +}) + +WIFI_NETWORK_BASE = cv.Schema({ + cv.GenerateID(): cv.declare_variable_id(WiFiAP), + cv.Optional(CONF_SSID): cv.ssid, + cv.Optional(CONF_PASSWORD): validate_password, + cv.Optional(CONF_CHANNEL): validate_channel, + cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, +}) + +WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend({ + +}) + +WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend({ + cv.Optional(CONF_BSSID): cv.mac_address, + cv.Optional(CONF_HIDDEN): cv.boolean, +}) + + +def validate(config): + if CONF_PASSWORD in config and CONF_SSID not in config: + raise cv.Invalid("Cannot have WiFi password without SSID!") + + if CONF_SSID in config: + network = {CONF_SSID: config.pop(CONF_SSID)} + if CONF_PASSWORD in config: + network[CONF_PASSWORD] = config.pop(CONF_PASSWORD) + if CONF_NETWORKS in config: + raise cv.Invalid("You cannot use the 'ssid:' option together with 'networks:'. Please " + "copy your network into the 'networks:' key") + config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network) + + if (CONF_NETWORKS not in config) and (CONF_AP not in config): + raise cv.Invalid("Please specify at least an SSID or an Access Point " + "to create.") + + if config.get(CONF_FAST_CONNECT, False): + networks = config.get(CONF_NETWORKS, []) + if not networks: + raise cv.Invalid("At least one network required for fast_connect!") + if len(networks) != 1: + raise cv.Invalid("Fast connect can only be used with one network!") + + if CONF_USE_ADDRESS not in config: + if CONF_MANUAL_IP in config: + use_address = str(config[CONF_MANUAL_IP][CONF_STATIC_IP]) + else: + use_address = CORE.name + config[CONF_DOMAIN] + config[CONF_USE_ADDRESS] = use_address + + return config + + +CONFIG_SCHEMA = cv.All(cv.Schema({ + cv.GenerateID(): cv.declare_variable_id(WiFiComponent), + cv.Optional(CONF_NETWORKS): cv.ensure_list(WIFI_NETWORK_STA), + + cv.Optional(CONF_SSID): cv.ssid, + cv.Optional(CONF_PASSWORD): validate_password, + cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, + + cv.Optional(CONF_AP): WIFI_NETWORK_AP, + cv.Optional(CONF_DOMAIN, default='.local'): cv.domain_name, + cv.Optional(CONF_REBOOT_TIMEOUT, default='5min'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_POWER_SAVE_MODE, default='NONE'): + cv.one_of(*WIFI_POWER_SAVE_MODES, upper=True), + cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean, + cv.Optional(CONF_USE_ADDRESS): cv.string_strict, + + cv.Optional('hostname'): cv.invalid("The hostname option has been removed in 1.11.0"), +}), validate) + + +def safe_ip(ip): + if ip is None: + return IPAddress(0, 0, 0, 0) + return IPAddress(*ip.args) + + +def manual_ip(config): + if config is None: + return None + return cg.StructInitializer( + ManualIP, + ('static_ip', safe_ip(config[CONF_STATIC_IP])), + ('gateway', safe_ip(config[CONF_GATEWAY])), + ('subnet', safe_ip(config[CONF_SUBNET])), + ('dns1', safe_ip(config.get(CONF_DNS1))), + ('dns2', safe_ip(config.get(CONF_DNS2))), + ) + + +def wifi_network(config, static_ip): + ap = cg.variable(config[CONF_ID], WiFiAP()) + if CONF_SSID in config: + cg.add(ap.set_ssid(config[CONF_SSID])) + if CONF_PASSWORD in config: + cg.add(ap.set_password(config[CONF_PASSWORD])) + if CONF_BSSID in config: + cg.add(ap.set_bssid([HexInt(i) for i in config[CONF_BSSID].parts])) + if CONF_HIDDEN in config: + cg.add(ap.set_hidden(config[CONF_HIDDEN])) + if CONF_CHANNEL in config: + cg.add(ap.set_channel(config[CONF_CHANNEL])) + if static_ip is not None: + cg.add(ap.set_manual_ip(manual_ip(static_ip))) + + return ap + + +@coroutine_with_priority(60.0) +def to_code(config): + rhs = WiFiComponent.new() + wifi = cg.Pvariable(config[CONF_ID], rhs) + cg.add(wifi.set_use_address(config[CONF_USE_ADDRESS])) + + for network in config.get(CONF_NETWORKS, []): + cg.add(wifi.add_sta(wifi_network(network, config.get(CONF_MANUAL_IP)))) + + if CONF_AP in config: + cg.add(wifi.set_ap(wifi_network(config[CONF_AP], config.get(CONF_MANUAL_IP)))) + + cg.add(wifi.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) + cg.add(wifi.set_power_save_mode(WIFI_POWER_SAVE_MODES[config[CONF_POWER_SAVE_MODE]])) + cg.add(wifi.set_fast_connect(config[CONF_FAST_CONNECT])) + + if CORE.is_esp8266: + cg.add_library('ESP8266WiFi', None) + + cg.add_define('USE_WIFI') + + # Register at end for OTA safe mode + yield cg.register_component(wifi, config) + + +@CONDITION_REGISTRY.register('wifi.connected', cv.Schema({})) +def wifi_connected_to_code(config, condition_id, template_arg, args): + rhs = WiFiConnectedCondition.new(template_arg) + type = WiFiConnectedCondition.template(template_arg) + yield cg.Pvariable(condition_id, rhs, type=type) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp new file mode 100644 index 000000000..ffea4982b --- /dev/null +++ b/esphome/components/wifi/wifi_component.cpp @@ -0,0 +1,545 @@ +#include "esphome/components/wifi/wifi_component.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif +#ifdef ARDUINO_ARCH_ESP8266 +#include +#endif + +#include +#include +#include "lwip/err.h" +#include "lwip/dns.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/esphal.h" +#include "esphome/core/util.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace wifi { + +static const char *TAG = "wifi"; + +float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; } + +void WiFiComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up WiFi..."); + + this->wifi_register_callbacks_(); + + bool ret = this->wifi_mode_(this->has_sta(), false); + if (!ret) { + this->mark_failed(); + return; + } + + if (this->has_sta()) { + this->wifi_disable_auto_connect_(); + delay(10); + + this->wifi_apply_power_save_(); + + if (this->fast_connect_) { + this->selected_ap_ = this->sta_[0]; + this->start_connecting(this->selected_ap_, false); + } else { + this->start_scanning(); + } + } else if (this->has_ap()) { + this->setup_ap_config_(); + } + + this->wifi_apply_hostname_(); + network_setup_mdns(); +} + +void WiFiComponent::loop() { + const uint32_t now = millis(); + + if (this->has_sta()) { + switch (this->state_) { + case WIFI_COMPONENT_STATE_COOLDOWN: { + this->status_set_warning(); + if (millis() - this->action_started_ > 5000) { + if (this->fast_connect_) { + this->start_connecting(this->sta_[0], false); + } else { + this->start_scanning(); + } + } + break; + } + case WIFI_COMPONENT_STATE_STA_SCANNING: { + this->status_set_warning(); + this->check_scanning_finished(); + break; + } + case WIFI_COMPONENT_STATE_STA_CONNECTING: + case WIFI_COMPONENT_STATE_STA_CONNECTING_2: { + this->status_set_warning(); + this->check_connecting_finished(); + break; + } + + case WIFI_COMPONENT_STATE_STA_CONNECTED: { + if (!this->is_connected()) { + ESP_LOGW(TAG, "WiFi Connection lost... Reconnecting..."); + this->retry_connect(); + } else { + this->status_clear_warning(); + this->last_connected_ = now; + } + break; + } + case WIFI_COMPONENT_STATE_OFF: + case WIFI_COMPONENT_STATE_AP: + break; + } + + if (!this->has_ap() && this->reboot_timeout_ != 0) { + if (now - this->last_connected_ > this->reboot_timeout_) { + ESP_LOGE(TAG, "Can't connect to WiFi, rebooting..."); + App.reboot(); + } + } + } + + network_tick_mdns(); +} + +WiFiComponent::WiFiComponent() { global_wifi_component = this; } + +bool WiFiComponent::has_ap() const { return !this->ap_.get_ssid().empty(); } +bool WiFiComponent::has_sta() const { return !this->sta_.empty(); } +void WiFiComponent::set_fast_connect(bool fast_connect) { this->fast_connect_ = fast_connect; } +IPAddress WiFiComponent::get_ip_address() { + if (this->has_sta()) + return this->wifi_sta_ip_(); + if (this->has_ap()) + return this->wifi_soft_ap_ip_(); + return {}; +} +std::string WiFiComponent::get_use_address() const { + if (this->use_address_.empty()) { + return App.get_name() + ".local"; + } + return this->use_address_; +} +void WiFiComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } +void WiFiComponent::setup_ap_config_() { + this->wifi_mode_({}, true); + + if (this->ap_setup_) + return; + + ESP_LOGCONFIG(TAG, "Setting up AP..."); + + ESP_LOGCONFIG(TAG, " AP SSID: '%s'", this->ap_.get_ssid().c_str()); + ESP_LOGCONFIG(TAG, " AP Password: '%s'", this->ap_.get_password().c_str()); + if (this->ap_.get_manual_ip().has_value()) { + auto manual = *this->ap_.get_manual_ip(); + ESP_LOGCONFIG(TAG, " AP Static IP: '%s'", manual.static_ip.toString().c_str()); + ESP_LOGCONFIG(TAG, " AP Gateway: '%s'", manual.gateway.toString().c_str()); + ESP_LOGCONFIG(TAG, " AP Subnet: '%s'", manual.subnet.toString().c_str()); + } + + this->ap_setup_ = this->wifi_start_ap_(this->ap_); + ESP_LOGCONFIG(TAG, " IP Address: %s", this->wifi_soft_ap_ip_().toString().c_str()); + + if (!this->has_sta()) { + this->state_ = WIFI_COMPONENT_STATE_AP; + } +} + +float WiFiComponent::get_loop_priority() const { + return 10.0f; // before other loop components +} +void WiFiComponent::set_ap(const WiFiAP &ap) { this->ap_ = ap; } +void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } + +void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { + ESP_LOGI(TAG, "WiFi Connecting to '%s'...", ap.get_ssid().c_str()); +#ifdef ESPHOME_LOG_HAS_VERBOSE + ESP_LOGV(TAG, "Connection Params:"); + ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str()); + if (ap.get_bssid().has_value()) { + bssid_t b = *ap.get_bssid(); + ESP_LOGV(TAG, " BSSID: %02X:%02X:%02X:%02X:%02X:%02X", b[0], b[1], b[2], b[3], b[4], b[5]); + } else { + ESP_LOGV(TAG, " BSSID: Not Set"); + } + ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.get_password().c_str()); + if (ap.get_channel().has_value()) { + ESP_LOGV(TAG, " Channel: %u", *ap.get_channel()); + } else { + ESP_LOGV(TAG, " Channel: Not Set"); + } + if (ap.get_manual_ip().has_value()) { + ManualIP m = *ap.get_manual_ip(); + ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.toString().c_str(), + m.gateway.toString().c_str(), m.subnet.toString().c_str(), m.dns1.toString().c_str(), + m.dns2.toString().c_str()); + } else { + ESP_LOGV(TAG, " Using DHCP IP"); + } + ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden())); +#endif + + if (!this->wifi_sta_connect_(ap)) { + ESP_LOGE(TAG, "wifi_sta_connect_ failed!"); + this->retry_connect(); + return; + } + + if (!two) { + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; + } else { + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; + } + this->action_started_ = millis(); +} + +void print_signal_bars(int8_t rssi, char *buf) { + // LOWER ONE QUARTER BLOCK + // Unicode: U+2582, UTF-8: E2 96 82 + // LOWER HALF BLOCK + // Unicode: U+2584, UTF-8: E2 96 84 + // LOWER THREE QUARTERS BLOCK + // Unicode: U+2586, UTF-8: E2 96 86 + // FULL BLOCK + // Unicode: U+2588, UTF-8: E2 96 88 + if (rssi >= -50) { + sprintf(buf, "\033[0;32m" // green + "\xe2\x96\x82" + "\xe2\x96\x84" + "\xe2\x96\x86" + "\xe2\x96\x88" + "\033[0m"); + } else if (rssi >= -65) { + sprintf(buf, "\033[0;33m" // yellow + "\xe2\x96\x82" + "\xe2\x96\x84" + "\xe2\x96\x86" + "\033[0;37m" + "\xe2\x96\x88" + "\033[0m"); + } else if (rssi >= -85) { + sprintf(buf, "\033[0;33m" // yellow + "\xe2\x96\x82" + "\xe2\x96\x84" + "\033[0;37m" + "\xe2\x96\x86" + "\xe2\x96\x88" + "\033[0m"); + } else { + sprintf(buf, "\033[0;31m" // red + "\xe2\x96\x82" + "\033[0;37m" + "\xe2\x96\x84" + "\xe2\x96\x86" + "\xe2\x96\x88" + "\033[0m"); + } +} + +void WiFiComponent::print_connect_params_() { + uint8_t *bssid = WiFi.BSSID(); + ESP_LOGCONFIG(TAG, " SSID: " LOG_SECRET("'%s'"), WiFi.SSID().c_str()); + ESP_LOGCONFIG(TAG, " IP Address: %s", WiFi.localIP().toString().c_str()); + ESP_LOGCONFIG(TAG, " BSSID: " LOG_SECRET("%02X:%02X:%02X:%02X:%02X:%02X"), bssid[0], bssid[1], bssid[2], bssid[3], + bssid[4], bssid[5]); + ESP_LOGCONFIG(TAG, " Hostname: '%s'", App.get_name().c_str()); + char signal_bars[50]; + int8_t rssi = WiFi.RSSI(); + print_signal_bars(rssi, signal_bars); + ESP_LOGCONFIG(TAG, " Signal strength: %d dB %s", rssi, signal_bars); + ESP_LOGCONFIG(TAG, " Channel: %d", WiFi.channel()); + ESP_LOGCONFIG(TAG, " Subnet: %s", WiFi.subnetMask().toString().c_str()); + ESP_LOGCONFIG(TAG, " Gateway: %s", WiFi.gatewayIP().toString().c_str()); + ESP_LOGCONFIG(TAG, " DNS1: %s", WiFi.dnsIP(0).toString().c_str()); + ESP_LOGCONFIG(TAG, " DNS2: %s", WiFi.dnsIP(1).toString().c_str()); +} + +void WiFiComponent::start_scanning() { + this->action_started_ = millis(); + ESP_LOGD(TAG, "Starting scan..."); + this->wifi_scan_start_(); + this->state_ = WIFI_COMPONENT_STATE_STA_SCANNING; +} + +void WiFiComponent::check_scanning_finished() { + if (!this->scan_done_) { + if (millis() - this->action_started_ > 30000) { + ESP_LOGE(TAG, "Scan timeout!"); + this->retry_connect(); + } + return; + } + this->scan_done_ = false; + + ESP_LOGD(TAG, "Found networks:"); + if (this->scan_result_.empty()) { + ESP_LOGD(TAG, " No network found!"); + this->retry_connect(); + return; + } + + for (auto &res : this->scan_result_) { + for (auto &ap : this->sta_) { + if (res.matches(ap)) { + res.set_matches(true); + break; + } + } + } + + std::stable_sort(this->scan_result_.begin(), this->scan_result_.end(), + [this](const WiFiScanResult &a, const WiFiScanResult &b) { + if (a.get_matches() && !b.get_matches()) + return true; + if (!a.get_matches() && b.get_matches()) + return false; + + return a.get_rssi() > b.get_rssi(); + }); + + for (auto &res : this->scan_result_) { + char bssid_s[18]; + auto bssid = res.get_bssid(); + sprintf(bssid_s, "%02X:%02X:%02X:%02X:%02X:%02X", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); + char signal_bars[50]; + print_signal_bars(res.get_rssi(), signal_bars); + + if (res.get_matches()) { + ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), + res.get_is_hidden() ? "(HIDDEN) " : "", bssid_s, signal_bars); + ESP_LOGD(TAG, " Channel: %u", res.get_channel()); + ESP_LOGD(TAG, " RSSI: %d dB", res.get_rssi()); + } else { + ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s, signal_bars); + } + } + + if (!this->scan_result_[0].get_matches()) { + ESP_LOGW(TAG, "No matching network found!"); + this->retry_connect(); + return; + } + + WiFiAP connect_params; + WiFiScanResult scan_res = this->scan_result_[0]; + for (auto &config : this->sta_) { + // search for matching STA config, at least one will match (from checks before) + if (!scan_res.matches(config)) { + continue; + } + + if (config.get_hidden()) { + // selected network is hidden, we use the data from the config + connect_params.set_hidden(true); + connect_params.set_ssid(config.get_ssid()); + // don't set BSSID and channel, there might be multiple hidden networks + // but we can't know which one is the correct one. Rely on probe-req with just SSID. + } else { + // selected network is visible, we use the data from the scan + // limit the connect params to only connect to exactly this network + // (network selection is done during scan phase). + connect_params.set_hidden(false); + connect_params.set_ssid(scan_res.get_ssid()); + connect_params.set_channel(scan_res.get_channel()); + connect_params.set_bssid(scan_res.get_bssid()); + } + // set manual IP+password (if any) + connect_params.set_manual_ip(config.get_manual_ip()); + connect_params.set_password(config.get_password()); + break; + } + + yield(); + + this->selected_ap_ = connect_params; + this->start_connecting(connect_params, false); +} + +void WiFiComponent::dump_config() { + ESP_LOGCONFIG(TAG, "WiFi:"); + this->print_connect_params_(); +} + +void WiFiComponent::check_connecting_finished() { + wl_status_t status = this->wifi_sta_status_(); + + if (status == WL_CONNECTED) { + ESP_LOGI(TAG, "WiFi connected!"); + this->print_connect_params_(); + + if (this->has_ap()) { + ESP_LOGD(TAG, "Disabling AP..."); + this->wifi_mode_({}, false); + } + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; + this->num_retried_ = 0; + return; + } + + uint32_t now = millis(); + if (now - this->action_started_ > 30000) { + ESP_LOGW(TAG, "Timeout while connecting to WiFi."); + this->retry_connect(); + return; + } + + if (this->error_from_callback_) { + ESP_LOGW(TAG, "Error while connecting to network."); + this->retry_connect(); + return; + } + + if (status == WL_IDLE_STATUS || status == WL_DISCONNECTED || status == WL_CONNECTION_LOST) { + // WL_DISCONNECTED is set while not connected yet. + // WL_IDLE_STATUS is set while we're waiting for the IP address. + // WL_CONNECTION_LOST happens on the ESP32 + return; + } + + if (status == WL_NO_SSID_AVAIL) { + ESP_LOGW(TAG, "WiFi network can not be found anymore."); + this->retry_connect(); + return; + } + + if (status == WL_CONNECT_FAILED) { + ESP_LOGW(TAG, "Connecting to WiFi network failed. Are the credentials wrong?"); + this->retry_connect(); + return; + } + + ESP_LOGW(TAG, "WiFi Unknown connection status %d", status); +} + +void WiFiComponent::retry_connect() { + if (this->num_retried_ > 5 || this->error_from_callback_) { + // If retry failed for more than 5 times, let's restart STA + ESP_LOGW(TAG, "Restarting WiFi adapter..."); + this->wifi_mode_(false, {}); + delay(100); + this->num_retried_ = 0; + } else { + this->num_retried_++; + } + this->error_from_callback_ = false; + if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) { + yield(); + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; + this->start_connecting(this->selected_ap_, true); + return; + } + + if (this->has_ap()) { + this->setup_ap_config_(); + } + this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; + this->action_started_ = millis(); +} + +bool WiFiComponent::can_proceed() { + if (this->has_ap() && !this->has_sta()) { + return true; + } + return this->is_connected(); +} +void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } +bool WiFiComponent::is_connected() { + return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && this->wifi_sta_status_() == WL_CONNECTED && + !this->error_from_callback_; +} +bool WiFiComponent::ready_for_ota() { + if (this->has_ap()) + return true; + return this->is_connected(); +} +void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; } + +std::string WiFiComponent::format_mac_addr(const uint8_t *mac) { + char buf[20]; + sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + return buf; +} +void WiFiComponent::on_safe_shutdown() { + // Disable WiFi interface on shutdown + this->wifi_mode_(false, false); +} + +bool sta_field_equal(const uint8_t *field_a, const uint8_t *field_b, int len) { + for (int i = 0; i < len; i++) { + uint8_t a = field_a[i]; + uint8_t b = field_b[i]; + if (a == b && a == 0) + break; + if (a == b) + continue; + + return false; + } + + return true; +} + +void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; } +void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; } +void WiFiAP::set_bssid(optional bssid) { this->bssid_ = bssid; } +void WiFiAP::set_password(const std::string &password) { this->password_ = password; } +void WiFiAP::set_channel(optional channel) { this->channel_ = channel; } +void WiFiAP::set_manual_ip(optional manual_ip) { this->manual_ip_ = manual_ip; } +void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; } +const std::string &WiFiAP::get_ssid() const { return this->ssid_; } +const optional &WiFiAP::get_bssid() const { return this->bssid_; } +const std::string &WiFiAP::get_password() const { return this->password_; } +const optional &WiFiAP::get_channel() const { return this->channel_; } +const optional &WiFiAP::get_manual_ip() const { return this->manual_ip_; } +bool WiFiAP::get_hidden() const { return this->hidden_; } + +WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const std::string &ssid, uint8_t channel, int8_t rssi, + bool with_auth, bool is_hidden) + : bssid_(bssid), ssid_(ssid), channel_(channel), rssi_(rssi), with_auth_(with_auth), is_hidden_(is_hidden) {} +bool WiFiScanResult::matches(const WiFiAP &config) { + if (config.get_hidden()) { + // User configured a hidden network, only match actually hidden networks + // don't match SSID + if (!this->is_hidden_) + return false; + } else if (!config.get_ssid().empty()) { + // check if SSID matches + if (config.get_ssid() != this->ssid_) + return false; + } else { + // network is configured without SSID - match other settings + } + // If BSSID configured, only match for correct BSSIDs + if (config.get_bssid().has_value() && *config.get_bssid() != this->bssid_) + return false; + // If PW given, only match for networks with auth (and vice versa) + if (config.get_password().empty() == this->with_auth_) + return false; + // If channel configured, only match networks on that channel. + if (config.get_channel().has_value() && *config.get_channel() != this->channel_) { + return false; + } + return true; +} +bool WiFiScanResult::get_matches() const { return this->matches_; } +void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; } +const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; } +const std::string &WiFiScanResult::get_ssid() const { return this->ssid_; } +uint8_t WiFiScanResult::get_channel() const { return this->channel_; } +int8_t WiFiScanResult::get_rssi() const { return this->rssi_; } +bool WiFiScanResult::get_with_auth() const { return this->with_auth_; } +bool WiFiScanResult::get_is_hidden() const { return this->is_hidden_; } + +WiFiComponent *global_wifi_component; + +} // namespace wifi +} // namespace esphome diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h new file mode 100644 index 000000000..a9e67f23c --- /dev/null +++ b/esphome/components/wifi/wifi_component.h @@ -0,0 +1,236 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" +#include +#include + +#ifdef ARDUINO_ARCH_ESP32 +#include +#include +#include +#endif + +#ifdef ARDUINO_ARCH_ESP8266 +#include +#include + +#ifdef ARDUINO_ESP8266_RELEASE_2_3_0 +extern "C" { +#include +}; +#endif +#endif + +namespace esphome { +namespace wifi { + +enum WiFiComponentState { + /** Nothing has been initialized yet. Internal AP, if configured, is disabled at this point. */ + WIFI_COMPONENT_STATE_OFF = 0, + /** WiFi is in cooldown mode because something went wrong, scanning will begin after a short period of time. */ + WIFI_COMPONENT_STATE_COOLDOWN, + /** WiFi is in STA-only mode and currently scanning for APs. */ + WIFI_COMPONENT_STATE_STA_SCANNING, + /** WiFi is in STA(+AP) mode and currently connecting to an AP. */ + WIFI_COMPONENT_STATE_STA_CONNECTING, + /** WiFi is in STA(+AP) mode and currently connecting to an AP a second time. + * + * This is required because for some reason ESPs don't like to connect to WiFi APs directly after + * a scan. + * */ + WIFI_COMPONENT_STATE_STA_CONNECTING_2, + /** WiFi is in STA(+AP) mode and successfully connected. */ + WIFI_COMPONENT_STATE_STA_CONNECTED, + /** WiFi is in AP-only mode and internal AP is already enabled. */ + WIFI_COMPONENT_STATE_AP, +}; + +/// Struct for setting static IPs in WiFiComponent. +struct ManualIP { + IPAddress static_ip; + IPAddress gateway; + IPAddress subnet; + IPAddress dns1; ///< The first DNS server. 0.0.0.0 for default. + IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default. +}; + +using bssid_t = std::array; + +class WiFiAP { + public: + void set_ssid(const std::string &ssid); + void set_bssid(bssid_t bssid); + void set_bssid(optional bssid); + void set_password(const std::string &password); + void set_channel(optional channel); + void set_manual_ip(optional manual_ip); + void set_hidden(bool hidden); + const std::string &get_ssid() const; + const optional &get_bssid() const; + const std::string &get_password() const; + const optional &get_channel() const; + const optional &get_manual_ip() const; + bool get_hidden() const; + + protected: + std::string ssid_; + optional bssid_; + std::string password_; + optional channel_; + optional manual_ip_; + bool hidden_{false}; +}; + +class WiFiScanResult { + public: + WiFiScanResult(const bssid_t &bssid, const std::string &ssid, uint8_t channel, int8_t rssi, bool with_auth, + bool is_hidden); + + bool matches(const WiFiAP &config); + + bool get_matches() const; + void set_matches(bool matches); + const bssid_t &get_bssid() const; + const std::string &get_ssid() const; + uint8_t get_channel() const; + int8_t get_rssi() const; + bool get_with_auth() const; + bool get_is_hidden() const; + + protected: + bool matches_{false}; + bssid_t bssid_; + std::string ssid_; + uint8_t channel_; + int8_t rssi_; + bool with_auth_; + bool is_hidden_; +}; + +enum WiFiPowerSaveMode { + WIFI_POWER_SAVE_NONE = 0, + WIFI_POWER_SAVE_LIGHT, + WIFI_POWER_SAVE_HIGH, +}; + +/// This component is responsible for managing the ESP WiFi interface. +class WiFiComponent : public Component { + public: + /// Construct a WiFiComponent. + WiFiComponent(); + + void add_sta(const WiFiAP &ap); + + /** Setup an Access Point that should be created if no connection to a station can be made. + * + * This can also be used without set_sta(). Then the AP will always be active. + * + * If both STA and AP are defined, then both will be enabled at startup, but if a connection to a station + * can be made, the AP will be turned off again. + */ + void set_ap(const WiFiAP &ap); + + void start_scanning(); + void check_scanning_finished(); + void start_connecting(const WiFiAP &ap, bool two); + void set_fast_connect(bool fast_connect); + + void check_connecting_finished(); + + void retry_connect(); + + bool can_proceed() override; + + bool ready_for_ota(); + + void set_reboot_timeout(uint32_t reboot_timeout); + + bool is_connected(); + + void set_power_save_mode(WiFiPowerSaveMode power_save); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + /// Setup WiFi interface. + void setup() override; + void dump_config() override; + /// WIFI setup_priority. + float get_setup_priority() const override; + float get_loop_priority() const override; + + /// Reconnect WiFi if required. + void loop() override; + + void on_safe_shutdown() override; + + bool has_sta() const; + bool has_ap() const; + + IPAddress get_ip_address(); + std::string get_use_address() const; + void set_use_address(const std::string &use_address); + + protected: + static std::string format_mac_addr(const uint8_t mac[6]); + void setup_ap_config_(); + void print_connect_params_(); + + bool wifi_mode_(optional sta, optional ap); + bool wifi_disable_auto_connect_(); + bool wifi_apply_power_save_(); + bool wifi_sta_ip_config_(optional manual_ip); + IPAddress wifi_sta_ip_(); + bool wifi_apply_hostname_(); + bool wifi_sta_connect_(WiFiAP ap); + void wifi_register_callbacks_(); + wl_status_t wifi_sta_status_(); + bool wifi_scan_start_(); + bool wifi_ap_ip_config_(optional manual_ip); + bool wifi_start_ap_(const WiFiAP &ap); + IPAddress wifi_soft_ap_ip_(); + +#ifdef ARDUINO_ARCH_ESP8266 + static void wifi_event_callback(System_Event_t *event); + void wifi_scan_done_callback_(void *arg, STATUS status); + static void s_wifi_scan_done_callback(void *arg, STATUS status); +#endif + +#ifdef ARDUINO_ARCH_ESP32 + void wifi_event_callback_(system_event_id_t event, system_event_info_t info); + void wifi_scan_done_callback_(); +#endif + + std::string use_address_; + std::vector sta_; + WiFiAP selected_ap_; + bool fast_connect_{false}; + + WiFiAP ap_; + WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; + uint32_t action_started_; + uint8_t num_retried_{0}; + uint32_t last_connected_{0}; + uint32_t reboot_timeout_{300000}; + WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; + bool error_from_callback_{false}; + std::vector scan_result_; + bool scan_done_{false}; + bool ap_setup_{false}; +}; + +extern WiFiComponent *global_wifi_component; + +template class WiFiConnectedCondition : public Condition { + public: + bool check(Ts... x) override; +}; + +template bool WiFiConnectedCondition::check(Ts... x) { + return global_wifi_component->is_connected(); +} + +} // namespace wifi +} // namespace esphome diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32.cpp new file mode 100644 index 000000000..1edfe11ce --- /dev/null +++ b/esphome/components/wifi/wifi_component_esp32.cpp @@ -0,0 +1,527 @@ +#include "esphome/components/wifi/wifi_component.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include + +#include +#include +#include "lwip/err.h" +#include "lwip/dns.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/esphal.h" +#include "esphome/core/application.h" +#include "esphome/core/util.h" + +namespace esphome { +namespace wifi { + +static const char *TAG = "wifi_esp32"; + +bool WiFiComponent::wifi_mode_(optional sta, optional ap) { + uint8_t current_mode = WiFi.getMode(); + bool current_sta = current_mode & 0b01; + bool current_ap = current_mode & 0b10; + bool sta_ = sta.value_or(current_sta); + bool ap_ = ap.value_or(current_ap); + if (current_sta == sta_ && current_ap == ap_) + return true; + + if (sta_ && !current_sta) { + ESP_LOGV(TAG, "Enabling STA."); + } else if (!sta_ && current_sta) { + ESP_LOGV(TAG, "Disabling STA."); + } + if (ap_ && !current_ap) { + ESP_LOGV(TAG, "Enabling AP."); + } else if (!ap_ && current_ap) { + ESP_LOGV(TAG, "Disabling AP."); + } + + uint8_t mode = 0; + if (sta_) + mode |= 0b01; + if (ap_) + mode |= 0b10; + bool ret = WiFi.mode(static_cast(mode)); + + if (!ret) { + ESP_LOGW(TAG, "Setting WiFi mode failed!"); + } + + return ret; +} +bool WiFiComponent::wifi_disable_auto_connect_() { + WiFi.setAutoReconnect(false); + return true; +} +bool WiFiComponent::wifi_apply_power_save_() { + wifi_ps_type_t power_save; + switch (this->power_save_) { + case WIFI_POWER_SAVE_LIGHT: + power_save = WIFI_PS_MIN_MODEM; + break; + case WIFI_POWER_SAVE_HIGH: + power_save = WIFI_PS_MAX_MODEM; + break; + case WIFI_POWER_SAVE_NONE: + default: + power_save = WIFI_PS_NONE; + break; + } + return esp_wifi_set_ps(power_save) == ESP_OK; +} +bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { + // enable STA + if (!this->wifi_mode_(true, {})) + return false; + + tcpip_adapter_dhcp_status_t dhcp_status; + tcpip_adapter_dhcpc_get_status(TCPIP_ADAPTER_IF_STA, &dhcp_status); + if (!manual_ip.has_value()) { + // Use DHCP client + if (dhcp_status != TCPIP_ADAPTER_DHCP_STARTED) { + esp_err_t err = tcpip_adapter_dhcpc_start(TCPIP_ADAPTER_IF_STA); + if (err != ESP_OK) { + ESP_LOGV(TAG, "Starting DHCP client failed! %d", err); + } + return err == ESP_OK; + } + return true; + } + + tcpip_adapter_ip_info_t info; + memset(&info, 0, sizeof(info)); + info.ip.addr = static_cast(manual_ip->static_ip); + info.gw.addr = static_cast(manual_ip->gateway); + info.netmask.addr = static_cast(manual_ip->subnet); + + esp_err_t dhcp_stop_ret = tcpip_adapter_dhcpc_stop(TCPIP_ADAPTER_IF_STA); + if (dhcp_stop_ret != ESP_OK) { + ESP_LOGV(TAG, "Stopping DHCP client failed! %d", dhcp_stop_ret); + } + + esp_err_t wifi_set_info_ret = tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_STA, &info); + if (wifi_set_info_ret != ESP_OK) { + ESP_LOGV(TAG, "Setting manual IP info failed! %s", esp_err_to_name(wifi_set_info_ret)); + } + + ip_addr_t dns; + dns.type = IPADDR_TYPE_V4; + if (uint32_t(manual_ip->dns1) != 0) { + dns.u_addr.ip4.addr = static_cast(manual_ip->dns1); + dns_setserver(0, &dns); + } + if (uint32_t(manual_ip->dns2) != 0) { + dns.u_addr.ip4.addr = static_cast(manual_ip->dns2); + dns_setserver(1, &dns); + } + + return true; +} + +IPAddress WiFiComponent::wifi_sta_ip_() { + if (!this->has_sta()) + return IPAddress(); + tcpip_adapter_ip_info_t ip; + tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip); + return IPAddress(ip.ip.addr); +} + +bool WiFiComponent::wifi_apply_hostname_() { + esp_err_t err = tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_STA, App.get_name().c_str()); + if (err != ESP_OK) { + ESP_LOGV(TAG, "Setting hostname failed: %d", err); + return false; + } + return true; +} +bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) { + // enable STA + if (!this->wifi_mode_(true, {})) + return false; + + wifi_config_t conf; + memset(&conf, 0, sizeof(conf)); + strcpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str()); + strcpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str()); + + if (ap.get_bssid().has_value()) { + conf.sta.bssid_set = 1; + memcpy(conf.sta.bssid, ap.get_bssid()->data(), 6); + } else { + conf.sta.bssid_set = 0; + } + if (ap.get_channel().has_value()) { + conf.sta.channel = *ap.get_channel(); + } + + esp_err_t err = esp_wifi_disconnect(); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_disconnect failed! %d", err); + return false; + } + + err = esp_wifi_set_config(WIFI_IF_STA, &conf); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_set_config failed! %d", err); + } + + if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { + return false; + } + + err = esp_wifi_connect(); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_wifi_connect failed! %d", err); + return false; + } + + return true; +} +const char *get_auth_mode_str(uint8_t mode) { + switch (mode) { + case WIFI_AUTH_OPEN: + return "OPEN"; + case WIFI_AUTH_WEP: + return "WEP"; + case WIFI_AUTH_WPA_PSK: + return "WPA PSK"; + case WIFI_AUTH_WPA2_PSK: + return "WPA2 PSK"; + case WIFI_AUTH_WPA_WPA2_PSK: + return "WPA/WPA2 PSK"; + case WIFI_AUTH_WPA2_ENTERPRISE: + return "WPA2 Enterprise"; + default: + return "UNKNOWN"; + } +} +std::string format_ip4_addr(const ip4_addr_t &ip) { + char buf[20]; + sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16), + uint8_t(ip.addr >> 24)); + return buf; +} +const char *get_op_mode_str(uint8_t mode) { + switch (mode) { + case WIFI_OFF: + return "OFF"; + case WIFI_STA: + return "STA"; + case WIFI_AP: + return "AP"; + case WIFI_AP_STA: + return "AP+STA"; + default: + return "UNKNOWN"; + } +} +const char *get_disconnect_reason_str(uint8_t reason) { + switch (reason) { + case WIFI_REASON_AUTH_EXPIRE: + return "Auth Expired"; + case WIFI_REASON_AUTH_LEAVE: + return "Auth Leave"; + case WIFI_REASON_ASSOC_EXPIRE: + return "Association Expired"; + case WIFI_REASON_ASSOC_TOOMANY: + return "Too Many Associations"; + case WIFI_REASON_NOT_AUTHED: + return "Not Authenticated"; + case WIFI_REASON_NOT_ASSOCED: + return "Not Associated"; + case WIFI_REASON_ASSOC_LEAVE: + return "Association Leave"; + case WIFI_REASON_ASSOC_NOT_AUTHED: + return "Association not Authenticated"; + case WIFI_REASON_DISASSOC_PWRCAP_BAD: + return "Disassociate Power Cap Bad"; + case WIFI_REASON_DISASSOC_SUPCHAN_BAD: + return "Disassociate Supported Channel Bad"; + case WIFI_REASON_IE_INVALID: + return "IE Invalid"; + case WIFI_REASON_MIC_FAILURE: + return "Mic Failure"; + case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT: + return "4-Way Handshake Timeout"; + case WIFI_REASON_GROUP_KEY_UPDATE_TIMEOUT: + return "Group Key Update Timeout"; + case WIFI_REASON_IE_IN_4WAY_DIFFERS: + return "IE In 4-Way Handshake Differs"; + case WIFI_REASON_GROUP_CIPHER_INVALID: + return "Group Cipher Invalid"; + case WIFI_REASON_PAIRWISE_CIPHER_INVALID: + return "Pairwise Cipher Invalid"; + case WIFI_REASON_AKMP_INVALID: + return "AKMP Invalid"; + case WIFI_REASON_UNSUPP_RSN_IE_VERSION: + return "Unsupported RSN IE version"; + case WIFI_REASON_INVALID_RSN_IE_CAP: + return "Invalid RSN IE Cap"; + case WIFI_REASON_802_1X_AUTH_FAILED: + return "802.1x Authentication Failed"; + case WIFI_REASON_CIPHER_SUITE_REJECTED: + return "Cipher Suite Rejected"; + case WIFI_REASON_BEACON_TIMEOUT: + return "Beacon Timeout"; + case WIFI_REASON_NO_AP_FOUND: + return "AP Not Found"; + case WIFI_REASON_AUTH_FAIL: + return "Authentication Failed"; + case WIFI_REASON_ASSOC_FAIL: + return "Association Failed"; + case WIFI_REASON_HANDSHAKE_TIMEOUT: + return "Handshake Failed"; + case WIFI_REASON_UNSPECIFIED: + default: + return "Unspecified"; + } +} +void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_info_t info) { + switch (event) { + case SYSTEM_EVENT_WIFI_READY: { + ESP_LOGV(TAG, "Event: WiFi ready"); + break; + } + case SYSTEM_EVENT_SCAN_DONE: { + auto it = info.scan_done; + ESP_LOGV(TAG, "Event: WiFi Scan Done status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id); + break; + } + case SYSTEM_EVENT_STA_START: { + ESP_LOGV(TAG, "Event: WiFi STA start"); + break; + } + case SYSTEM_EVENT_STA_STOP: { + ESP_LOGV(TAG, "Event: WiFi STA stop"); + break; + } + case SYSTEM_EVENT_STA_CONNECTED: { + auto it = info.connected; + char buf[33]; + memcpy(buf, it.ssid, it.ssid_len); + buf[it.ssid_len] = '\0'; + ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, + format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); + break; + } + case SYSTEM_EVENT_STA_DISCONNECTED: { + auto it = info.disconnected; + char buf[33]; + memcpy(buf, it.ssid, it.ssid_len); + buf[it.ssid_len] = '\0'; + ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason=%s", buf, + format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + break; + } + case SYSTEM_EVENT_STA_AUTHMODE_CHANGE: { + auto it = info.auth_change; + ESP_LOGV(TAG, "Event: Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), + get_auth_mode_str(it.new_mode)); + break; + } + case SYSTEM_EVENT_STA_GOT_IP: { + auto it = info.got_ip.ip_info; + ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(it.ip).c_str(), + format_ip4_addr(it.gw).c_str()); + break; + } + case SYSTEM_EVENT_STA_LOST_IP: { + ESP_LOGV(TAG, "Event: Lost IP"); + break; + } + case SYSTEM_EVENT_AP_START: { + ESP_LOGV(TAG, "Event: WiFi AP start"); + break; + } + case SYSTEM_EVENT_AP_STOP: { + ESP_LOGV(TAG, "Event: WiFi AP stop"); + break; + } + case SYSTEM_EVENT_AP_STACONNECTED: { + auto it = info.sta_connected; + ESP_LOGV(TAG, "Event: AP client connected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); + break; + } + case SYSTEM_EVENT_AP_STADISCONNECTED: { + auto it = info.sta_disconnected; + ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); + break; + } + case SYSTEM_EVENT_AP_STAIPASSIGNED: { + ESP_LOGV(TAG, "Event: AP client assigned IP"); + break; + } + case SYSTEM_EVENT_AP_PROBEREQRECVED: { + auto it = info.ap_probereqrecved; + ESP_LOGV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + break; + } + default: + break; + } + + if (event == SYSTEM_EVENT_STA_DISCONNECTED) { + uint8_t reason = info.disconnected.reason; + if (reason == WIFI_REASON_AUTH_EXPIRE || reason == WIFI_REASON_BEACON_TIMEOUT || + reason == WIFI_REASON_NO_AP_FOUND || reason == WIFI_REASON_ASSOC_FAIL || + reason == WIFI_REASON_HANDSHAKE_TIMEOUT) { + err_t err = esp_wifi_disconnect(); + if (err != ESP_OK) { + ESP_LOGV(TAG, "Disconnect failed: %s", esp_err_to_name(err)); + } + this->error_from_callback_ = true; + } + } + if (event == SYSTEM_EVENT_SCAN_DONE) { + this->wifi_scan_done_callback_(); + } +} +void WiFiComponent::wifi_register_callbacks_() { + auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2); + WiFi.onEvent(f); + WiFi.persistent(false); +} +wl_status_t WiFiComponent::wifi_sta_status_() { return WiFi.status(); } +bool WiFiComponent::wifi_scan_start_() { + // enable STA + if (!this->wifi_mode_(true, {})) + return false; + + // need to use WiFi because of WiFiScanClass allocations :( + int16_t err = WiFi.scanNetworks(true, true, false, 200); + if (err != WIFI_SCAN_RUNNING) { + ESP_LOGV(TAG, "WiFi.scanNetworks failed! %d", err); + return false; + } + + return true; +} +void WiFiComponent::wifi_scan_done_callback_() { + this->scan_result_.clear(); + + int16_t num = WiFi.scanComplete(); + if (num < 0) + return; + + this->scan_result_.reserve(static_cast(num)); + for (int i = 0; i < num; i++) { + String ssid = WiFi.SSID(i); + wifi_auth_mode_t authmode = WiFi.encryptionType(i); + int32_t rssi = WiFi.RSSI(i); + uint8_t *bssid = WiFi.BSSID(i); + int32_t channel = WiFi.channel(i); + + WiFiScanResult scan({bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]}, std::string(ssid.c_str()), + channel, rssi, authmode != WIFI_AUTH_OPEN, ssid.length() == 0); + this->scan_result_.push_back(scan); + } + WiFi.scanDelete(); + this->scan_done_ = true; +} +bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { + esp_err_t err; + + // enable AP + if (!this->wifi_mode_({}, true)) + return false; + + tcpip_adapter_ip_info_t info; + memset(&info, 0, sizeof(info)); + if (manual_ip.has_value()) { + info.ip.addr = static_cast(manual_ip->static_ip); + info.gw.addr = static_cast(manual_ip->gateway); + info.netmask.addr = static_cast(manual_ip->subnet); + } else { + info.ip.addr = static_cast(IPAddress(192, 168, 4, 1)); + info.gw.addr = static_cast(IPAddress(192, 168, 4, 1)); + info.netmask.addr = static_cast(IPAddress(255, 255, 255, 0)); + } + tcpip_adapter_dhcp_status_t dhcp_status; + tcpip_adapter_dhcps_get_status(TCPIP_ADAPTER_IF_AP, &dhcp_status); + err = tcpip_adapter_dhcps_stop(TCPIP_ADAPTER_IF_AP); + if (err != ESP_OK) { + ESP_LOGV(TAG, "tcpip_adapter_dhcps_stop failed! %d", err); + return false; + } + + err = tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_AP, &info); + if (err != ESP_OK) { + ESP_LOGV(TAG, "tcpip_adapter_set_ip_info failed! %d", err); + return false; + } + + dhcps_lease_t lease; + lease.enable = true; + IPAddress start_address = info.ip.addr; + start_address[3] += 99; + lease.start_ip.addr = static_cast(start_address); + ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.toString().c_str()); + start_address[3] += 100; + lease.end_ip.addr = static_cast(start_address); + ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.toString().c_str()); + err = tcpip_adapter_dhcps_option(TCPIP_ADAPTER_OP_SET, TCPIP_ADAPTER_REQUESTED_IP_ADDRESS, &lease, sizeof(lease)); + + if (err != ESP_OK) { + ESP_LOGV(TAG, "tcpip_adapter_dhcps_option failed! %d", err); + return false; + } + + err = tcpip_adapter_dhcps_start(TCPIP_ADAPTER_IF_AP); + + if (err != ESP_OK) { + ESP_LOGV(TAG, "tcpip_adapter_dhcps_start failed! %d", err); + return false; + } + + return true; +} +bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { + // enable AP + if (!this->wifi_mode_({}, true)) + return false; + + wifi_config_t conf; + memset(&conf, 0, sizeof(conf)); + strcpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str()); + conf.ap.channel = ap.get_channel().value_or(1); + conf.ap.ssid_hidden = ap.get_ssid().size(); + conf.ap.max_connection = 5; + conf.ap.beacon_interval = 100; + + if (ap.get_password().empty()) { + conf.ap.authmode = WIFI_AUTH_OPEN; + *conf.ap.password = 0; + } else { + conf.ap.authmode = WIFI_AUTH_WPA2_PSK; + strcpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str()); + } + + esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &conf); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_set_config failed! %d", err); + return false; + } + + yield(); + + if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!"); + return false; + } + + return true; +} +IPAddress WiFiComponent::wifi_soft_ap_ip_() { + tcpip_adapter_ip_info_t ip; + tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip); + return IPAddress(ip.ip.addr); +} + +} // namespace wifi +} // namespace esphome + +#endif diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp new file mode 100644 index 000000000..5030e87e2 --- /dev/null +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -0,0 +1,594 @@ +#include "esphome/components/wifi/wifi_component.h" + +#ifdef ARDUINO_ARCH_ESP8266 + +#include + +#include +#include +#include "lwip/err.h" +#include "lwip/dns.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/esphal.h" +#include "esphome/core/util.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace wifi { + +static const char *TAG = "wifi_esp8266"; + +bool WiFiComponent::wifi_mode_(optional sta, optional ap) { + uint8_t current_mode = wifi_get_opmode(); + bool current_sta = current_mode & 0b01; + bool current_ap = current_mode & 0b10; + bool target_sta = sta.value_or(current_sta); + bool target_ap = ap.value_or(current_ap); + if (current_sta == target_sta && current_ap == target_ap) + return true; + + if (target_sta && !current_sta) { + ESP_LOGV(TAG, "Enabling STA."); + } else if (!target_sta && current_sta) { + ESP_LOGV(TAG, "Disabling STA."); + // Stop DHCP client when disabling STA + // See https://github.com/esp8266/Arduino/pull/5703 + wifi_station_dhcpc_stop(); + } + if (target_ap && !current_ap) { + ESP_LOGV(TAG, "Enabling AP."); + } else if (!target_ap && current_ap) { + ESP_LOGV(TAG, "Disabling AP."); + } + + ETS_UART_INTR_DISABLE(); + uint8_t mode = 0; + if (target_sta) + mode |= 0b01; + if (target_ap) + mode |= 0b10; + bool ret = wifi_set_opmode_current(mode); + ETS_UART_INTR_ENABLE(); + + if (!ret) { + ESP_LOGW(TAG, "Setting WiFi mode failed!"); + } + + return ret; +} +bool WiFiComponent::wifi_disable_auto_connect_() { + bool ret1, ret2; + ETS_UART_INTR_DISABLE(); + ret1 = wifi_station_set_auto_connect(0); + ret2 = wifi_station_set_reconnect_policy(false); + ETS_UART_INTR_ENABLE(); + + if (!ret1 || !ret2) { + ESP_LOGV(TAG, "Disabling Auto-Connect failed!"); + } + + return ret1 && ret2; +} +bool WiFiComponent::wifi_apply_power_save_() { + sleep_type_t power_save; + switch (this->power_save_) { + case WIFI_POWER_SAVE_LIGHT: + power_save = LIGHT_SLEEP_T; + break; + case WIFI_POWER_SAVE_HIGH: + power_save = MODEM_SLEEP_T; + break; + case WIFI_POWER_SAVE_NONE: + default: + power_save = NONE_SLEEP_T; + break; + } + return wifi_set_sleep_type(power_save); +} +bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { + // enable STA + if (!this->wifi_mode_(true, {})) + return false; + + enum dhcp_status dhcp_status = wifi_station_dhcpc_status(); + if (!manual_ip.has_value()) { + // Use DHCP client + if (dhcp_status != DHCP_STARTED) { + bool ret = wifi_station_dhcpc_start(); + if (!ret) { + ESP_LOGV(TAG, "Starting DHCP client failed!"); + } + return ret; + } + return true; + } + + bool ret = true; + + struct ip_info info {}; + info.ip.addr = static_cast(manual_ip->static_ip); + info.gw.addr = static_cast(manual_ip->gateway); + info.netmask.addr = static_cast(manual_ip->subnet); + + if (dhcp_status == DHCP_STARTED) { + bool dhcp_stop_ret = wifi_station_dhcpc_stop(); + if (!dhcp_stop_ret) { + ESP_LOGV(TAG, "Stopping DHCP client failed!"); + ret = false; + } + } + bool wifi_set_info_ret = wifi_set_ip_info(STATION_IF, &info); + if (!wifi_set_info_ret) { + ESP_LOGV(TAG, "Setting manual IP info failed!"); + ret = false; + } + + ip_addr_t dns; + if (uint32_t(manual_ip->dns1) != 0) { + dns.addr = static_cast(manual_ip->dns1); + dns_setserver(0, &dns); + } + if (uint32_t(manual_ip->dns2) != 0) { + dns.addr = static_cast(manual_ip->dns2); + dns_setserver(1, &dns); + } + + return ret; +} + +IPAddress WiFiComponent::wifi_sta_ip_() { + if (!this->has_sta()) + return {}; + struct ip_info ip {}; + wifi_get_ip_info(STATION_IF, &ip); + return {ip.ip.addr}; +} +bool WiFiComponent::wifi_apply_hostname_() { + bool ret = wifi_station_set_hostname(const_cast(App.get_name().c_str())); + if (!ret) { + ESP_LOGV(TAG, "Setting WiFi Hostname failed!"); + } + return ret; +} + +bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) { + // enable STA + if (!this->wifi_mode_(true, {})) + return false; + + ETS_UART_INTR_DISABLE(); + wifi_station_disconnect(); + ETS_UART_INTR_ENABLE(); + + struct station_config conf {}; + memset(&conf, 0, sizeof(conf)); + strcpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str()); + strcpy(reinterpret_cast(conf.password), ap.get_password().c_str()); + + if (ap.get_bssid().has_value()) { + conf.bssid_set = 1; + memcpy(conf.bssid, ap.get_bssid()->data(), 6); + } else { + conf.bssid_set = 0; + } + +#ifndef ARDUINO_ESP8266_RELEASE_2_3_0 + if (ap.get_password().empty()) { + conf.threshold.authmode = AUTH_OPEN; + } else { + conf.threshold.authmode = AUTH_WPA_PSK; + } + conf.threshold.rssi = -127; +#endif + + ETS_UART_INTR_DISABLE(); + bool ret = wifi_station_set_config_current(&conf); + ETS_UART_INTR_ENABLE(); + + if (!ret) { + ESP_LOGV(TAG, "Setting WiFi Station config failed!"); + return false; + } + + if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { + return false; + } + + ETS_UART_INTR_DISABLE(); + ret = wifi_station_connect(); + ETS_UART_INTR_ENABLE(); + if (!ret) { + ESP_LOGV(TAG, "wifi_station_connect failed!"); + return false; + } + + if (ap.get_channel().has_value()) { + ret = wifi_set_channel(*ap.get_channel()); + if (!ret) { + ESP_LOGV(TAG, "wifi_set_channel failed!"); + return false; + } + } + + return true; +} + +class WiFiMockClass : public ESP8266WiFiGenericClass { + public: + static void _event_callback(void *event) { ESP8266WiFiGenericClass::_eventCallback(event); } // NOLINT +}; + +const char *get_auth_mode_str(uint8_t mode) { + switch (mode) { + case AUTH_OPEN: + return "OPEN"; + case AUTH_WEP: + return "WEP"; + case AUTH_WPA_PSK: + return "WPA PSK"; + case AUTH_WPA2_PSK: + return "WPA2 PSK"; + case AUTH_WPA_WPA2_PSK: + return "WPA/WPA2 PSK"; + default: + return "UNKNOWN"; + } +} +#ifdef ipv4_addr +std::string format_ip_addr(struct ipv4_addr ip) { + char buf[20]; + sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16), + uint8_t(ip.addr >> 24)); + return buf; +} +#else +std::string format_ip_addr(struct ip_addr ip) { + char buf[20]; + sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16), + uint8_t(ip.addr >> 24)); + return buf; +} +#endif +const char *get_op_mode_str(uint8_t mode) { + switch (mode) { + case WIFI_OFF: + return "OFF"; + case WIFI_STA: + return "STA"; + case WIFI_AP: + return "AP"; + case WIFI_AP_STA: + return "AP+STA"; + default: + return "UNKNOWN"; + } +} +const char *get_disconnect_reason_str(uint8_t reason) { + switch (reason) { + case REASON_AUTH_EXPIRE: + return "Auth Expired"; + case REASON_AUTH_LEAVE: + return "Auth Leave"; + case REASON_ASSOC_EXPIRE: + return "Association Expired"; + case REASON_ASSOC_TOOMANY: + return "Too Many Associations"; + case REASON_NOT_AUTHED: + return "Not Authenticated"; + case REASON_NOT_ASSOCED: + return "Not Associated"; + case REASON_ASSOC_LEAVE: + return "Association Leave"; + case REASON_ASSOC_NOT_AUTHED: + return "Association not Authenticated"; + case REASON_DISASSOC_PWRCAP_BAD: + return "Disassociate Power Cap Bad"; + case REASON_DISASSOC_SUPCHAN_BAD: + return "Disassociate Supported Channel Bad"; + case REASON_IE_INVALID: + return "IE Invalid"; + case REASON_MIC_FAILURE: + return "Mic Failure"; + case REASON_4WAY_HANDSHAKE_TIMEOUT: + return "4-Way Handshake Timeout"; + case REASON_GROUP_KEY_UPDATE_TIMEOUT: + return "Group Key Update Timeout"; + case REASON_IE_IN_4WAY_DIFFERS: + return "IE In 4-Way Handshake Differs"; + case REASON_GROUP_CIPHER_INVALID: + return "Group Cipher Invalid"; + case REASON_PAIRWISE_CIPHER_INVALID: + return "Pairwise Cipher Invalid"; + case REASON_AKMP_INVALID: + return "AKMP Invalid"; + case REASON_UNSUPP_RSN_IE_VERSION: + return "Unsupported RSN IE version"; + case REASON_INVALID_RSN_IE_CAP: + return "Invalid RSN IE Cap"; + case REASON_802_1X_AUTH_FAILED: + return "802.1x Authentication Failed"; + case REASON_CIPHER_SUITE_REJECTED: + return "Cipher Suite Rejected"; + case REASON_BEACON_TIMEOUT: + return "Beacon Timeout"; + case REASON_NO_AP_FOUND: + return "AP Not Found"; + case REASON_AUTH_FAIL: + return "Authentication Failed"; + case REASON_ASSOC_FAIL: + return "Association Failed"; + case REASON_HANDSHAKE_TIMEOUT: + return "Handshake Failed"; + case REASON_UNSPECIFIED: + default: + return "Unspecified"; + } +} + +void WiFiComponent::wifi_event_callback(System_Event_t *event) { +#ifdef ESPHOME_LOG_HAS_VERBOSE + // TODO: this callback is called while in cont context, so delay will fail + // We need to defer the log messages until we're out of this context + // only affects verbose log level + // reproducible by enabling verbose log level and letting the ESP disconnect and + // then reconnect to WiFi. + switch (event->event) { + case EVENT_STAMODE_CONNECTED: { + auto it = event->event_info.connected; + char buf[33]; + memcpy(buf, it.ssid, it.ssid_len); + buf[it.ssid_len] = '\0'; + ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_addr(it.bssid).c_str(), + it.channel); + break; + } + case EVENT_STAMODE_DISCONNECTED: { + auto it = event->event_info.disconnected; + char buf[33]; + memcpy(buf, it.ssid, it.ssid_len); + buf[it.ssid_len] = '\0'; + ESP_LOGV(TAG, "Event: Disconnected ssid='%s' bssid=%s reason='%s'", buf, format_mac_addr(it.bssid).c_str(), + get_disconnect_reason_str(it.reason)); + break; + } + case EVENT_STAMODE_AUTHMODE_CHANGE: { + auto it = event->event_info.auth_change; + ESP_LOGV(TAG, "Event: Changed AuthMode old=%s new=%s", get_auth_mode_str(it.old_mode), + get_auth_mode_str(it.new_mode)); + break; + } + case EVENT_STAMODE_GOT_IP: { + auto it = event->event_info.got_ip; + ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(), + format_ip_addr(it.gw).c_str(), format_ip_addr(it.mask).c_str()); + break; + } + case EVENT_STAMODE_DHCP_TIMEOUT: { + ESP_LOGW(TAG, "Event: Getting IP address timeout"); + break; + } + case EVENT_SOFTAPMODE_STACONNECTED: { + auto it = event->event_info.sta_connected; + ESP_LOGV(TAG, "Event: AP client connected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); + break; + } + case EVENT_SOFTAPMODE_STADISCONNECTED: { + auto it = event->event_info.sta_disconnected; + ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); + break; + } + case EVENT_SOFTAPMODE_PROBEREQRECVED: { + auto it = event->event_info.ap_probereqrecved; + ESP_LOGV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + break; + } +#ifndef ARDUINO_ESP8266_RELEASE_2_3_0 + case EVENT_OPMODE_CHANGED: { + auto it = event->event_info.opmode_changed; + ESP_LOGV(TAG, "Event: Changed Mode old=%s new=%s", get_op_mode_str(it.old_opmode), + get_op_mode_str(it.new_opmode)); + break; + } + case EVENT_SOFTAPMODE_DISTRIBUTE_STA_IP: { + auto it = event->event_info.distribute_sta_ip; + ESP_LOGV(TAG, "Event: AP Distribute Station IP MAC=%s IP=%s aid=%u", format_mac_addr(it.mac).c_str(), + format_ip_addr(it.ip).c_str(), it.aid); + break; + } +#endif + default: + break; + } +#endif + + if (event->event == EVENT_STAMODE_DISCONNECTED) { + global_wifi_component->error_from_callback_ = true; + } + + WiFiMockClass::_event_callback(event); +} + +void WiFiComponent::wifi_register_callbacks_() { wifi_set_event_handler_cb(&WiFiComponent::wifi_event_callback); } +wl_status_t WiFiComponent::wifi_sta_status_() { + station_status_t status = wifi_station_get_connect_status(); + switch (status) { + case STATION_GOT_IP: + return WL_CONNECTED; + case STATION_NO_AP_FOUND: + return WL_NO_SSID_AVAIL; + case STATION_CONNECT_FAIL: + case STATION_WRONG_PASSWORD: + return WL_CONNECT_FAILED; + case STATION_IDLE: + return WL_IDLE_STATUS; + case STATION_CONNECTING: + default: + return WL_DISCONNECTED; + } +} +bool WiFiComponent::wifi_scan_start_() { + static bool FIRST_SCAN = false; + + // enable STA + if (!this->wifi_mode_(true, {})) + return false; + + station_status_t sta_status = wifi_station_get_connect_status(); + if (sta_status != STATION_GOT_IP && sta_status != STATION_IDLE) { + wifi_station_disconnect(); + } + + struct scan_config config {}; + memset(&config, 0, sizeof(config)); + config.ssid = nullptr; + config.bssid = nullptr; + config.channel = 0; + config.show_hidden = 1; +#ifndef ARDUINO_ESP8266_RELEASE_2_3_0 + config.scan_type = WIFI_SCAN_TYPE_ACTIVE; + if (FIRST_SCAN) { + config.scan_time.active.min = 100; + config.scan_time.active.max = 200; + } else { + config.scan_time.active.min = 400; + config.scan_time.active.max = 500; + } +#endif + FIRST_SCAN = false; + bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback); + if (!ret) { + ESP_LOGV(TAG, "wifi_station_scan failed!"); + return false; + } + + return ret; +} +void WiFiComponent::s_wifi_scan_done_callback(void *arg, STATUS status) { + global_wifi_component->wifi_scan_done_callback_(arg, status); +} + +void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { + this->scan_result_.clear(); + + if (status != OK) { + ESP_LOGV(TAG, "Scan failed! %d", status); + return; + } + auto *head = reinterpret_cast(arg); + for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) { + WiFiScanResult res({it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, + std::string(reinterpret_cast(it->ssid), it->ssid_len), it->channel, it->rssi, + it->authmode != AUTH_OPEN, it->is_hidden != 0); + this->scan_result_.push_back(res); + } + this->scan_done_ = true; +} +bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { + // enable AP + if (!this->wifi_mode_({}, true)) + return false; + + struct ip_info info {}; + if (manual_ip.has_value()) { + info.ip.addr = static_cast(manual_ip->static_ip); + info.gw.addr = static_cast(manual_ip->gateway); + info.netmask.addr = static_cast(manual_ip->subnet); + } else { + info.ip.addr = static_cast(IPAddress(192, 168, 4, 1)); + info.gw.addr = static_cast(IPAddress(192, 168, 4, 1)); + info.netmask.addr = static_cast(IPAddress(255, 255, 255, 0)); + } + + if (wifi_softap_dhcps_status() == DHCP_STARTED) { + if (!wifi_softap_dhcps_stop()) { + ESP_LOGV(TAG, "Stopping DHCP server failed!"); + } + } + + if (!wifi_set_ip_info(SOFTAP_IF, &info)) { + ESP_LOGV(TAG, "Setting SoftAP info failed!"); + return false; + } + + struct dhcps_lease lease {}; + IPAddress start_address = info.ip.addr; + start_address[3] += 99; + lease.start_ip.addr = static_cast(start_address); + ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.toString().c_str()); + start_address[3] += 100; + lease.end_ip.addr = static_cast(start_address); + ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.toString().c_str()); + if (!wifi_softap_set_dhcps_lease(&lease)) { + ESP_LOGV(TAG, "Setting SoftAP DHCP lease failed!"); + return false; + } + + // lease time 1440 minutes (=24 hours) + if (!wifi_softap_set_dhcps_lease_time(1440)) { + ESP_LOGV(TAG, "Setting SoftAP DHCP lease time failed!"); + return false; + } + + uint8_t mode = 1; + // bit0, 1 enables router information from ESP8266 SoftAP DHCP server. + if (!wifi_softap_set_dhcps_offer_option(OFFER_ROUTER, &mode)) { + ESP_LOGV(TAG, "wifi_softap_set_dhcps_offer_option failed!"); + return false; + } + + if (!wifi_softap_dhcps_start()) { + ESP_LOGV(TAG, "Starting SoftAP DHCPS failed!"); + return false; + } + + return true; +} +bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { + // enable AP + if (!this->wifi_mode_({}, true)) + return false; + + struct softap_config conf {}; + strcpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str()); + conf.ssid_len = static_cast(ap.get_ssid().size()); + conf.channel = ap.get_channel().value_or(1); + conf.ssid_hidden = 0; + conf.max_connection = 5; + conf.beacon_interval = 100; + + if (ap.get_password().empty()) { + conf.authmode = AUTH_OPEN; + *conf.password = 0; + } else { + conf.authmode = AUTH_WPA2_PSK; + strcpy(reinterpret_cast(conf.password), ap.get_password().c_str()); + } + + ETS_UART_INTR_DISABLE(); + bool ret = wifi_softap_set_config_current(&conf); + ETS_UART_INTR_ENABLE(); + + if (!ret) { + ESP_LOGV(TAG, "wifi_softap_set_config_current failed!"); + return false; + } + + if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!"); + return false; + } + + return true; +} +IPAddress WiFiComponent::wifi_soft_ap_ip_() { + struct ip_info ip {}; + wifi_get_ip_info(SOFTAP_IF, &ip); + return {ip.ip.addr}; +} + +} // namespace wifi +} // namespace esphome + +#endif diff --git a/esphome/components/wifi_info/__init__.py b/esphome/components/wifi_info/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py new file mode 100644 index 000000000..56a073ba3 --- /dev/null +++ b/esphome/components/wifi_info/text_sensor.py @@ -0,0 +1,39 @@ +from esphome.components import text_sensor +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_BSSID, CONF_ID, CONF_IP_ADDRESS, CONF_NAME, CONF_SSID +from esphome.core import coroutine + +DEPENDENCIES = ['wifi'] + +wifi_info_ns = cg.esphome_ns.namespace('wifi_info') +IPAddressWiFiInfo = wifi_info_ns.class_('IPAddressWiFiInfo', text_sensor.TextSensor, cg.Component) +SSIDWiFiInfo = wifi_info_ns.class_('SSIDWiFiInfo', text_sensor.TextSensor, cg.Component) +BSSIDWiFiInfo = wifi_info_ns.class_('BSSIDWiFiInfo', text_sensor.TextSensor, cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.Optional(CONF_IP_ADDRESS): cv.nameable(text_sensor.TEXT_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_variable_id(IPAddressWiFiInfo), + })), + cv.Optional(CONF_SSID): cv.nameable(text_sensor.TEXT_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_variable_id(SSIDWiFiInfo), + })), + cv.Optional(CONF_BSSID): cv.nameable(text_sensor.TEXT_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_variable_id(BSSIDWiFiInfo), + })), +}) + + +@coroutine +def setup_conf(config, key): + if key in config: + conf = config[key] + var = cg.new_Pvariable(conf[CONF_ID], conf[CONF_NAME]) + yield cg.register_component(var, conf) + yield text_sensor.register_text_sensor(var, conf) + + +def to_code(config): + yield setup_conf(config, CONF_IP_ADDRESS) + yield setup_conf(config, CONF_SSID) + yield setup_conf(config, CONF_BSSID) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h new file mode 100644 index 000000000..c3685db06 --- /dev/null +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -0,0 +1,61 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/wifi/wifi_component.h" + +namespace esphome { +namespace wifi_info { + +class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor { + public: + IPAddressWiFiInfo(const std::string &name) : TextSensor(name) {} + void loop() override { + IPAddress ip = WiFi.localIP(); + if (ip != this->last_ip_) { + this->last_ip_ = ip; + this->publish_state(ip.toString().c_str()); + } + } + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + protected: + IPAddress last_ip_; +}; + +class SSIDWiFiInfo : public Component, public text_sensor::TextSensor { + public: + SSIDWiFiInfo(const std::string &name) : TextSensor(name) {} + void loop() override { + String ssid = WiFi.SSID(); + if (this->last_ssid_ != ssid.c_str()) { + this->last_ssid_ = std::string(ssid.c_str()); + this->publish_state(this->last_ssid_); + } + } + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + protected: + std::string last_ssid_; +}; + +class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor { + public: + BSSIDWiFiInfo(const std::string &name) : TextSensor(name) {} + void loop() override { + uint8_t *bssid = WiFi.BSSID(); + if (memcmp(bssid, this->last_bssid_.data(), 6) != 0) { + std::copy(bssid, bssid + 6, this->last_bssid_.data()); + char buf[30]; + sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); + this->publish_state(buf); + } + } + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + protected: + wifi::bssid_t last_bssid_; +}; + +} // namespace wifi_info +} // namespace esphome diff --git a/esphome/components/wifi_signal/__init__.py b/esphome/components/wifi_signal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/esphome/components/wifi_signal/sensor.py b/esphome/components/wifi_signal/sensor.py new file mode 100644 index 000000000..48134919c --- /dev/null +++ b/esphome/components/wifi_signal/sensor.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import CONF_ID, CONF_NAME, CONF_UPDATE_INTERVAL, CONF_ICON, \ + CONF_UNIT_OF_MEASUREMENT, CONF_ACCURACY_DECIMALS, ICON_WIFI, UNIT_DECIBEL + +DEPENDENCIES = ['wifi'] +wifi_signal_ns = cg.esphome_ns.namespace('wifi_signal') +WiFiSignalSensor = wifi_signal_ns.class_('WiFiSignalSensor', sensor.PollingSensorComponent) + +CONFIG_SCHEMA = cv.nameable(sensor.SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_variable_id(WiFiSignalSensor), + cv.Optional(CONF_UPDATE_INTERVAL, default='60s'): cv.update_interval, + + cv.Optional(CONF_ICON, default=ICON_WIFI): sensor.icon, + cv.Optional(CONF_UNIT_OF_MEASUREMENT, default=UNIT_DECIBEL): sensor.unit_of_measurement, + cv.Optional(CONF_ACCURACY_DECIMALS, default=0): sensor.accuracy_decimals, +}).extend(cv.COMPONENT_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID], config[CONF_NAME], config[CONF_UPDATE_INTERVAL]) + yield cg.register_component(var, config) + yield sensor.register_sensor(var, config) diff --git a/esphome/components/wifi_signal/wifi_signal_sensor.cpp b/esphome/components/wifi_signal/wifi_signal_sensor.cpp new file mode 100644 index 000000000..7b2f010c0 --- /dev/null +++ b/esphome/components/wifi_signal/wifi_signal_sensor.cpp @@ -0,0 +1,12 @@ +#include "wifi_signal_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace wifi_signal { + +static const char *TAG = "wifi_signal.sensor"; + +void WiFiSignalSensor::dump_config() { LOG_SENSOR("", "WiFi Signal", this); } + +} // namespace wifi_signal +} // namespace esphome diff --git a/esphome/components/wifi_signal/wifi_signal_sensor.h b/esphome/components/wifi_signal/wifi_signal_sensor.h new file mode 100644 index 000000000..623d7acda --- /dev/null +++ b/esphome/components/wifi_signal/wifi_signal_sensor.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/wifi/wifi_component.h" + +namespace esphome { +namespace wifi_signal { + +class WiFiSignalSensor : public sensor::PollingSensorComponent { + public: + explicit WiFiSignalSensor(const std::string &name, uint32_t update_interval) + : sensor::PollingSensorComponent(name, update_interval) {} + + void update() override { this->publish_state(WiFi.RSSI()); } + void dump_config() override; + + std::string unique_id() override { return get_mac_address() + "-wifisignal"; } + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } +}; + +} // namespace wifi_signal +} // namespace esphome diff --git a/esphome/components/xiaomi_ble/__init__.py b/esphome/components/xiaomi_ble/__init__.py new file mode 100644 index 000000000..3e0132d7e --- /dev/null +++ b/esphome/components/xiaomi_ble/__init__.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components.esp32_ble_tracker import CONF_ESP32_BLE_ID, ESPBTDeviceListener, \ + ESP_BLE_DEVICE_SCHEMA +from esphome.const import CONF_ID + +DEPENDENCIES = ['esp32_ble_tracker'] + +xiaomi_ble_ns = cg.esphome_ns.namespace('xiaomi_ble') +XiaomiListener = xiaomi_ble_ns.class_('XiaomiListener', cg.Component, ESPBTDeviceListener) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_variable_id(XiaomiListener), +}).extend(ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + hub = yield cg.get_variable(config[CONF_ESP32_BLE_ID]) + var = cg.new_Pvariable(config[CONF_ID], hub) + yield cg.register_component(var, config) diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp new file mode 100644 index 000000000..8addbe640 --- /dev/null +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -0,0 +1,147 @@ +#include "xiaomi_ble.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_ble { + +static const char *TAG = "xiaomi_ble"; + +bool parse_xiaomi_data_byte(uint8_t data_type, const uint8_t *data, uint8_t data_length, XiaomiParseResult &result) { + switch (data_type) { + case 0x0D: { // temperature+humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 % + if (data_length != 4) + return false; + const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + const int16_t humidity = uint16_t(data[2]) | (uint16_t(data[3]) << 8); + result.temperature = temperature / 10.0f; + result.humidity = humidity / 10.0f; + return true; + } + case 0x0A: { // battery, 1 byte, 8-bit unsigned integer, 1 % + if (data_length != 1) + return false; + result.battery_level = data[0]; + return true; + } + case 0x06: { // humidity, 2 bytes, 16-bit signed integer (LE), 0.1 % + if (data_length != 2) + return false; + const int16_t humidity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.humidity = humidity / 10.0f; + return true; + } + case 0x04: { // temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C + if (data_length != 2) + return false; + const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.temperature = temperature / 10.0f; + return true; + } + case 0x09: { // conductivity, 2 bytes, 16-bit unsigned integer (LE), 1 µS/cm + if (data_length != 2) + return false; + const uint16_t conductivity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.conductivity = conductivity; + return true; + } + case 0x07: { // illuminance, 3 bytes, 24-bit unsigned integer (LE), 1 lx + if (data_length != 3) + return false; + const uint32_t illuminance = uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16); + result.illuminance = illuminance; + return true; + } + case 0x08: { // soil moisture, 1 byte, 8-bit unsigned integer, 1 % + if (data_length != 1) + return false; + result.moisture = data[0]; + return true; + } + default: + return false; + } +} +optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &device) { + if (!device.get_service_data_uuid().has_value()) { + // ESP_LOGVV(TAG, "Xiaomi no service data"); + return {}; + } + + if (!device.get_service_data_uuid()->contains(0x95, 0xFE)) { + // ESP_LOGVV(TAG, "Xiaomi no service data UUID magic bytes"); + return {}; + } + + const auto *raw = reinterpret_cast(device.get_service_data().data()); + + if (device.get_service_data().size() < 14) { + // ESP_LOGVV(TAG, "Xiaomi service data too short!"); + return {}; + } + + bool is_mijia = (raw[1] & 0x20) == 0x20 && raw[2] == 0xAA && raw[3] == 0x01; + bool is_miflora = (raw[1] & 0x20) == 0x20 && raw[2] == 0x98 && raw[3] == 0x00; + + if (!is_mijia && !is_miflora) { + // ESP_LOGVV(TAG, "Xiaomi no magic bytes"); + return {}; + } + + uint8_t raw_offset = is_mijia ? 11 : 12; + + const uint8_t raw_type = raw[raw_offset]; + const uint8_t data_length = raw[raw_offset + 2]; + const uint8_t *data = &raw[raw_offset + 3]; + const uint8_t expected_length = data_length + raw_offset + 3; + const uint8_t actual_length = device.get_service_data().size(); + if (expected_length != actual_length) { + // ESP_LOGV(TAG, "Xiaomi %s data length mismatch (%u != %d)", type, expected_length, actual_length); + return {}; + } + XiaomiParseResult result; + result.type = is_miflora ? XiaomiParseResult::TYPE_MIFLORA : XiaomiParseResult::TYPE_MIJIA; + bool success = parse_xiaomi_data_byte(raw_type, data, data_length, result); + if (!success) + return {}; + return result; +} + +bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + auto res = parse_xiaomi(device); + if (!res.has_value()) + return false; + + const char *name = res->type == XiaomiParseResult::TYPE_MIFLORA ? "Mi Flora" : "Mi Jia"; + + ESP_LOGD(TAG, "Got Xiaomi %s:", name); + + if (res->temperature.has_value()) { + ESP_LOGD(TAG, " Temperature: %.1f°C", *res->temperature); + } + if (res->humidity.has_value()) { + ESP_LOGD(TAG, " Humidity: %.1f%%", *res->humidity); + } + if (res->battery_level.has_value()) { + ESP_LOGD(TAG, " Battery Level: %.0f%%", *res->battery_level); + } + if (res->conductivity.has_value()) { + ESP_LOGD(TAG, " Conductivity: %.0fµS/cm", *res->conductivity); + } + if (res->illuminance.has_value()) { + ESP_LOGD(TAG, " Illuminance: %.0flx", *res->illuminance); + } + if (res->moisture.has_value()) { + ESP_LOGD(TAG, " Moisture: %.0f%%", *res->moisture); + } + + return true; +} +void XiaomiListener::setup() { this->setup_ble(); } +XiaomiListener::XiaomiListener(esp32_ble_tracker::ESP32BLETracker *parent) : ESPBTDeviceListener(parent) {} + +} // namespace xiaomi_ble +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h new file mode 100644 index 000000000..df70fd2fc --- /dev/null +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_ble { + +struct XiaomiParseResult { + enum { TYPE_MIJIA, TYPE_MIFLORA } type; + optional temperature; + optional humidity; + optional battery_level; + optional conductivity; + optional illuminance; + optional moisture; +}; + +bool parse_xiaomi_data_byte(uint8_t data_type, const uint8_t *data, uint8_t data_length, XiaomiParseResult &result); + +optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &device); + +class XiaomiListener : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + XiaomiListener(esp32_ble_tracker::ESP32BLETracker *parent); + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void setup() override; +}; + +} // namespace xiaomi_ble +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_miflora/__init__.py b/esphome/components/xiaomi_miflora/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/esphome/components/xiaomi_miflora/sensor.py b/esphome/components/xiaomi_miflora/sensor.py new file mode 100644 index 000000000..2b7d823c4 --- /dev/null +++ b/esphome/components/xiaomi_miflora/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.esp32_ble_tracker import CONF_ESP32_BLE_ID, ESPBTDeviceListener, \ + ESP_BLE_DEVICE_SCHEMA +from esphome.const import CONF_BATTERY_LEVEL, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + CONF_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_ACCURACY_DECIMALS, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ + CONF_MOISTURE, CONF_ILLUMINANCE, ICON_BRIGHTNESS_5, UNIT_LUX, CONF_CONDUCTIVITY, \ + UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_miflora_ns = cg.esphome_ns.namespace('xiaomi_miflora') +XiaomiMiflora = xiaomi_miflora_ns.class_('XiaomiMiflora', ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_variable_id(XiaomiMiflora), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): cv.nameable(sensor.SENSOR_SCHEMA.extend({ + cv.Optional(CONF_UNIT_OF_MEASUREMENT, default=UNIT_CELSIUS): sensor.unit_of_measurement, + cv.Optional(CONF_ICON, default=ICON_THERMOMETER): sensor.icon, + cv.Optional(CONF_ACCURACY_DECIMALS, default=1): sensor.accuracy_decimals + })), + cv.Optional(CONF_MOISTURE): cv.nameable(sensor.SENSOR_SCHEMA.extend({ + cv.Optional(CONF_UNIT_OF_MEASUREMENT, default=UNIT_PERCENT): sensor.unit_of_measurement, + cv.Optional(CONF_ICON, default=ICON_WATER_PERCENT): sensor.icon, + cv.Optional(CONF_ACCURACY_DECIMALS, default=0): sensor.accuracy_decimals + })), + cv.Optional(CONF_ILLUMINANCE): cv.nameable(sensor.SENSOR_SCHEMA.extend({ + cv.Optional(CONF_UNIT_OF_MEASUREMENT, default=UNIT_LUX): sensor.unit_of_measurement, + cv.Optional(CONF_ICON, default=ICON_BRIGHTNESS_5): sensor.icon, + cv.Optional(CONF_ACCURACY_DECIMALS, default=0): sensor.accuracy_decimals + })), + cv.Optional(CONF_CONDUCTIVITY): cv.nameable(sensor.SENSOR_SCHEMA.extend({ + cv.Optional(CONF_UNIT_OF_MEASUREMENT, default=UNIT_MICROSIEMENS_PER_CENTIMETER): + sensor.unit_of_measurement, + cv.Optional(CONF_ICON, default=ICON_FLOWER): sensor.icon, + cv.Optional(CONF_ACCURACY_DECIMALS, default=0): sensor.accuracy_decimals + })), + cv.Optional(CONF_BATTERY_LEVEL): cv.nameable(sensor.SENSOR_SCHEMA.extend({ + cv.Optional(CONF_UNIT_OF_MEASUREMENT, default=UNIT_PERCENT): sensor.unit_of_measurement, + cv.Optional(CONF_ICON, default=ICON_BATTERY): sensor.icon, + cv.Optional(CONF_ACCURACY_DECIMALS, default=0): sensor.accuracy_decimals + })), +}).extend(ESP_BLE_DEVICE_SCHEMA) + + +def to_code(config): + hub = yield cg.get_variable(config[CONF_ESP32_BLE_ID]) + var = cg.new_Pvariable(config[CONF_ID], hub, config[CONF_MAC_ADDRESS].as_hex) + yield cg.register_component(var, config) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_MOISTURE in config: + sens = yield sensor.new_sensor(config[CONF_MOISTURE]) + cg.add(var.set_moisture(sens)) + if CONF_ILLUMINANCE in config: + sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE]) + cg.add(var.set_illuminance(sens)) + if CONF_CONDUCTIVITY in config: + sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY]) + cg.add(var.set_conductivity(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_miflora/xiaomi_miflora.cpp b/esphome/components/xiaomi_miflora/xiaomi_miflora.cpp new file mode 100644 index 000000000..966c78a1a --- /dev/null +++ b/esphome/components/xiaomi_miflora/xiaomi_miflora.cpp @@ -0,0 +1,23 @@ +#include "xiaomi_miflora.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_miflora { + +static const char *TAG = "xiaomi_miflora"; + +void XiaomiMiflora::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi Mijia"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Moisture", this->moisture_); + LOG_SENSOR(" ", "Conductivity", this->conductivity_); + LOG_SENSOR(" ", "Illuminance", this->illuminance_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +} // namespace xiaomi_miflora +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_miflora/xiaomi_miflora.h b/esphome/components/xiaomi_miflora/xiaomi_miflora.h new file mode 100644 index 000000000..987e0cc40 --- /dev/null +++ b/esphome/components/xiaomi_miflora/xiaomi_miflora.h @@ -0,0 +1,59 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_miflora { + +class XiaomiMiflora : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + XiaomiMiflora(esp32_ble_tracker::ESP32BLETracker *parent, uint64_t address) + : ESPBTDeviceListener(parent), address_(address) {} + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { + if (device.address_uint64() != this->address_) + return false; + + auto res = xiaomi_ble::parse_xiaomi(device); + if (!res.has_value()) + return false; + + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->moisture.has_value() && this->moisture_ != nullptr) + this->moisture_->publish_state(*res->moisture); + if (res->conductivity.has_value() && this->conductivity_ != nullptr) + this->conductivity_->publish_state(*res->conductivity); + if (res->illuminance.has_value() && this->illuminance_ != nullptr) + this->illuminance_->publish_state(*res->illuminance); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + return true; + } + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } + void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } + void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + + protected: + uint64_t address_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *moisture_{nullptr}; + sensor::Sensor *conductivity_{nullptr}; + sensor::Sensor *illuminance_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_miflora +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_mijia/__init__.py b/esphome/components/xiaomi_mijia/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/esphome/components/xiaomi_mijia/sensor.py b/esphome/components/xiaomi_mijia/sensor.py new file mode 100644 index 000000000..0a9046880 --- /dev/null +++ b/esphome/components/xiaomi_mijia/sensor.py @@ -0,0 +1,50 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.components.esp32_ble_tracker import CONF_ESP32_BLE_ID, ESPBTDeviceListener, \ + ESP_BLE_DEVICE_SCHEMA +from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + CONF_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_ACCURACY_DECIMALS, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_mijia_ns = cg.esphome_ns.namespace('xiaomi_mijia') +XiaomiMijia = xiaomi_mijia_ns.class_('XiaomiMijia', ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_variable_id(XiaomiMijia), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): cv.nameable(sensor.SENSOR_SCHEMA.extend({ + cv.Optional(CONF_UNIT_OF_MEASUREMENT, default=UNIT_CELSIUS): sensor.unit_of_measurement, + cv.Optional(CONF_ICON, default=ICON_THERMOMETER): sensor.icon, + cv.Optional(CONF_ACCURACY_DECIMALS, default=1): sensor.accuracy_decimals + })), + cv.Optional(CONF_HUMIDITY): cv.nameable(sensor.SENSOR_SCHEMA.extend({ + cv.Optional(CONF_UNIT_OF_MEASUREMENT, default=UNIT_PERCENT): sensor.unit_of_measurement, + cv.Optional(CONF_ICON, default=ICON_WATER_PERCENT): sensor.icon, + cv.Optional(CONF_ACCURACY_DECIMALS, default=1): sensor.accuracy_decimals + })), + cv.Optional(CONF_BATTERY_LEVEL): cv.nameable(sensor.SENSOR_SCHEMA.extend({ + cv.Optional(CONF_UNIT_OF_MEASUREMENT, default=UNIT_PERCENT): sensor.unit_of_measurement, + cv.Optional(CONF_ICON, default=ICON_BATTERY): sensor.icon, + cv.Optional(CONF_ACCURACY_DECIMALS, default=0): sensor.accuracy_decimals + })), +}).extend(ESP_BLE_DEVICE_SCHEMA) + + +def to_code(config): + hub = yield cg.get_variable(config[CONF_ESP32_BLE_ID]) + var = cg.new_Pvariable(config[CONF_ID], hub, config[CONF_MAC_ADDRESS].as_hex) + yield cg.register_component(var, config) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_mijia/xiaomi_mijia.cpp b/esphome/components/xiaomi_mijia/xiaomi_mijia.cpp new file mode 100644 index 000000000..544af32d7 --- /dev/null +++ b/esphome/components/xiaomi_mijia/xiaomi_mijia.cpp @@ -0,0 +1,21 @@ +#include "xiaomi_mijia.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_mijia { + +static const char *TAG = "xiaomi_mijia"; + +void XiaomiMijia::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi Mijia"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +} // namespace xiaomi_mijia +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_mijia/xiaomi_mijia.h b/esphome/components/xiaomi_mijia/xiaomi_mijia.h new file mode 100644 index 000000000..e5b947e6b --- /dev/null +++ b/esphome/components/xiaomi_mijia/xiaomi_mijia.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_mijia { + +class XiaomiMijia : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + XiaomiMijia(esp32_ble_tracker::ESP32BLETracker *parent, uint64_t address) + : ESPBTDeviceListener(parent), address_(address) {} + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { + if (device.address_uint64() != this->address_) + return false; + + auto res = xiaomi_ble::parse_xiaomi(device); + if (!res.has_value()) + return false; + + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + return true; + } + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + + protected: + uint64_t address_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_mijia +} // namespace esphome + +#endif diff --git a/esphome/config.py b/esphome/config.py index 469b1305a..101e1d385 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -1,9 +1,10 @@ from __future__ import print_function -from collections import OrderedDict +import collections import importlib import logging import re +import os.path import voluptuous as vol @@ -13,7 +14,7 @@ from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS from esphome.core import CORE, EsphomeError from esphome.helpers import color, indent from esphome.py_compat import text_type -from esphome.util import safe_print +from esphome.util import safe_print, OrderedDict # pylint: disable=unused-import, wrong-import-order from typing import List, Optional, Tuple, Union # noqa @@ -26,46 +27,135 @@ _LOGGER = logging.getLogger(__name__) _COMPONENT_CACHE = {} -def get_component(domain): +class ComponentManifest(object): + def __init__(self, module, base_components_path, is_core=False, is_platform=False): + self.module = module + self._is_core = is_core + self.is_platform = is_platform + self.base_components_path = base_components_path + + @property + def is_platform_component(self): + return getattr(self.module, 'IS_PLATFORM_COMPONENT', False) + + @property + def config_schema(self): + return getattr(self.module, 'CONFIG_SCHEMA', None) + + @property + def is_multi_conf(self): + return getattr(self.module, 'MULTI_CONF', False) + + @property + def to_code(self): + return getattr(self.module, 'to_code', None) + + @property + def esp_platforms(self): + return getattr(self.module, 'ESP_PLATFORMS', ESP_PLATFORMS) + + @property + def dependencies(self): + return getattr(self.module, 'DEPENDENCIES', []) + + @property + def conflicts_with(self): + return getattr(self.module, 'CONFLICTS_WITH', []) + + @property + def auto_load(self): + return getattr(self.module, 'AUTO_LOAD', []) + + @property + def to_code_priority(self): + return getattr(self.module, 'TO_CODE_PRIORITY', []) + + def _get_flags_set(self, name, config): + if not hasattr(self.module, name): + return set() + obj = getattr(self.module, name) + if callable(obj): + obj = obj(config) + if obj is None: + return set() + if not isinstance(obj, (list, tuple, set)): + obj = [obj] + return set(obj) + + @property + def source_files(self): + if self._is_core: + core_p = os.path.abspath(os.path.join(os.path.dirname(__file__), 'core')) + source_files = core.find_source_files(os.path.join(core_p, 'dummy')) + ret = {} + for f in source_files: + ret['esphome/core/{}'.format(f)] = os.path.join(core_p, f) + return ret + + source_files = core.find_source_files(self.module.__file__) + ret = {} + # Make paths absolute + directory = os.path.abspath(os.path.dirname(self.module.__file__)) + for x in source_files: + full_file = os.path.join(directory, x) + rel = os.path.relpath(full_file, self.base_components_path) + # Always use / for C++ include names + rel = rel.replace(os.sep, '/') + target_file = 'esphome/components/{}'.format(rel) + ret[target_file] = full_file + return ret + + +CORE_COMPONENTS_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), 'components')) + + +def _lookup_module(domain, is_platform): if domain in _COMPONENT_CACHE: return _COMPONENT_CACHE[domain] path = 'esphome.components.{}'.format(domain) try: module = importlib.import_module(path) - except (ImportError, ValueError) as err: - _LOGGER.debug(err) + except ImportError: + return None + except Exception: # pylint: disable=broad-except + import traceback + _LOGGER.error("Unable to load component %s:", domain) + traceback.print_exc() + return None else: - _COMPONENT_CACHE[domain] = module - return module + manif = ComponentManifest(module, CORE_COMPONENTS_PATH, is_platform=is_platform) + _COMPONENT_CACHE[domain] = manif + return manif - _LOGGER.error("Unable to find component %s", domain) - return None + +def get_component(domain): + assert '.' not in domain + return _lookup_module(domain, False) def get_platform(domain, platform): - return get_component("{}.{}".format(domain, platform)) + full = '{}.{}'.format(platform, domain) + return _lookup_module(full, True) -def is_platform_component(component): - return hasattr(component, 'PLATFORM_SCHEMA') +_COMPONENT_CACHE['esphome'] = ComponentManifest( + core_config, CORE_COMPONENTS_PATH, is_core=True, is_platform=False, +) def iter_components(config): for domain, conf in config.items(): - if domain == CONF_ESPHOME: - yield CONF_ESPHOME, core_config, conf - continue component = get_component(domain) - if getattr(component, 'MULTI_CONF', False): + if component.is_multi_conf: for conf_ in conf: yield domain, component, conf_ else: yield domain, component, conf - if is_platform_component(component): + if component.is_platform_component: for p_config in conf: p_name = u"{}.{}".format(domain, p_config[CONF_PLATFORM]) - platform = get_component(p_name) + platform = get_platform(domain, p_config[CONF_PLATFORM]) yield p_name, platform, p_config @@ -229,8 +319,12 @@ def validate_config(config): # Step 1: Load everything result.add_domain([CONF_ESPHOME], CONF_ESPHOME) result[CONF_ESPHOME] = config[CONF_ESPHOME] - + config_queue = collections.deque() for domain, conf in config.items(): + config_queue.append((domain, conf)) + + while config_queue: + domain, conf = config_queue.popleft() domain = str(domain) if domain == CONF_ESPHOME or domain.startswith(u'.'): skip_paths.append([domain]) @@ -245,12 +339,11 @@ def validate_config(config): skip_paths.append([domain]) continue - if not isinstance(conf, list) and getattr(component, 'MULTI_CONF', False): + if component.is_multi_conf and not isinstance(conf, list): result[domain] = conf = [conf] success = True - dependencies = getattr(component, 'DEPENDENCIES', []) - for dependency in dependencies: + for dependency in component.dependencies: if dependency not in config: result.add_error(u"Component {} requires component {}".format(domain, dependency), [domain]) @@ -260,8 +353,7 @@ def validate_config(config): continue success = True - conflicts_with = getattr(component, 'CONFLICTS_WITH', []) - for conflict in conflicts_with: + for conflict in component.conflicts_with: if conflict in config: result.add_error(u"Component {} cannot be used together with component {}" u"".format(domain, conflict), [domain]) @@ -270,14 +362,23 @@ def validate_config(config): skip_paths.append([domain]) continue - esp_platforms = getattr(component, 'ESP_PLATFORMS', ESP_PLATFORMS) - if CORE.esp_platform not in esp_platforms: + for load in component.auto_load: + if load not in config: + conf = core.AutoLoad() + config[load] = conf + config_queue.append((load, conf)) + + if CORE.esp_platform not in component.esp_platforms: result.add_error(u"Component {} doesn't support {}.".format(domain, CORE.esp_platform), [domain]) skip_paths.append([domain]) continue - if not hasattr(component, 'PLATFORM_SCHEMA'): + if not component.is_platform_component: + if component.config_schema is None and not isinstance(conf, core.AutoLoad): + result.add_error(u"Component {} cannot be loaded via YAML (no CONFIG_SCHEMA)." + u"".format(domain), [domain]) + skip_paths.append([domain]) continue result.remove_domain([domain], domain) @@ -304,8 +405,7 @@ def validate_config(config): continue success = True - dependencies = getattr(platform, 'DEPENDENCIES', []) - for dependency in dependencies: + for dependency in platform.dependencies: if dependency not in config: result.add_error(u"Platform {} requires component {}" u"".format(p_domain, dependency), [domain, i]) @@ -315,8 +415,7 @@ def validate_config(config): continue success = True - conflicts_with = getattr(platform, 'CONFLICTS_WITH', []) - for conflict in conflicts_with: + for conflict in platform.conflicts_with: if conflict in config: result.add_error(u"Platform {} cannot be used together with component {}" u"".format(p_domain, conflict), [domain, i]) @@ -325,13 +424,23 @@ def validate_config(config): skip_paths.append([domain, i]) continue - esp_platforms = getattr(platform, 'ESP_PLATFORMS', ESP_PLATFORMS) - if CORE.esp_platform not in esp_platforms: + for load in platform.auto_load: + if load not in config: + conf = core.AutoLoad() + config[load] = conf + config_queue.append((load, conf)) + + if CORE.esp_platform not in platform.esp_platforms: result.add_error(u"Platform {} doesn't support {}." u"".format(p_domain, CORE.esp_platform), [domain, i]) skip_paths.append([domain, i]) continue + if platform.config_schema is None: + result.add_error(u"Platform {} cannot be loaded via YAML (no PLATFORM_SCHEMA)." + u"".format(p_domain), [domain, i]) + skip_paths.append([domain]) + # Step 2: Validate configuration try: result[CONF_ESPHOME] = core_config.CONFIG_SCHEMA(result[CONF_ESPHOME]) @@ -344,25 +453,24 @@ def validate_config(config): continue component = get_component(domain) - if hasattr(component, 'CONFIG_SCHEMA'): - multi_conf = getattr(component, 'MULTI_CONF', False) + if not component.is_platform_component: + if component.config_schema is None: + continue - if multi_conf: + if component.is_multi_conf: for i, conf_ in enumerate(conf): try: - validated = component.CONFIG_SCHEMA(conf_) + validated = component.config_schema(conf_) result[domain][i] = validated except vol.Invalid as ex: _comp_error(ex, [domain, i]) else: try: - validated = component.CONFIG_SCHEMA(conf) + validated = component.config_schema(conf) result[domain] = validated except vol.Invalid as ex: _comp_error(ex, [domain]) continue - - if not hasattr(component, 'PLATFORM_SCHEMA'): continue for i, p_config in enumerate(conf): @@ -371,12 +479,19 @@ def validate_config(config): p_name = p_config['platform'] platform = get_platform(domain, p_name) - if hasattr(platform, 'PLATFORM_SCHEMA'): + if platform.config_schema is not None: + # Remove 'platform' key for validation + input_conf = OrderedDict(p_config) + platform_val = input_conf.pop('platform') try: - p_validated = platform.PLATFORM_SCHEMA(p_config) + p_validated = platform.config_schema(input_conf) except vol.Invalid as ex: _comp_error(ex, [domain, i]) continue + if not isinstance(p_validated, OrderedDict): + p_validated = OrderedDict(p_validated) + p_validated['platform'] = platform_val + p_validated.move_to_end('platform', last=False) result[domain][i] = p_validated if not result.errors: @@ -574,7 +689,7 @@ def strip_default_ids(config): to_remove = [] for i, x in enumerate(config): x = config[i] = strip_default_ids(x) - if isinstance(x, core.ID) and not x.is_manual: + if (isinstance(x, core.ID) and not x.is_manual) or isinstance(x, core.AutoLoad): to_remove.append(x) for x in to_remove: config.remove(x) @@ -582,7 +697,7 @@ def strip_default_ids(config): to_remove = [] for k, v in config.items(): v = config[k] = strip_default_ids(v) - if isinstance(v, core.ID) and not v.is_manual: + if (isinstance(v, core.ID) and not v.is_manual) or isinstance(v, core.AutoLoad): to_remove.append(k) for k in to_remove: config.pop(k) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index e41dba35a..0c2321cb5 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -2,19 +2,19 @@ """Helpers for config validation using voluptuous.""" from __future__ import print_function -from datetime import datetime import logging import os import re import uuid as uuid_ +from datetime import datetime import voluptuous as vol from esphome import core from esphome.const import CONF_AVAILABILITY, CONF_COMMAND_TOPIC, CONF_DISCOVERY, CONF_ID, \ - CONF_INTERNAL, CONF_NAME, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_PLATFORM, \ - CONF_RETAIN, CONF_SETUP_PRIORITY, CONF_STATE_TOPIC, CONF_TOPIC, ESP_PLATFORM_ESP32, \ - ESP_PLATFORM_ESP8266, CONF_HOUR, CONF_MINUTE, CONF_SECOND + CONF_INTERNAL, CONF_NAME, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, \ + CONF_RETAIN, CONF_SETUP_PRIORITY, CONF_STATE_TOPIC, CONF_TOPIC, \ + CONF_HOUR, CONF_MINUTE, CONF_SECOND, CONF_VALUE from esphome.core import CORE, HexInt, IPAddress, Lambda, TimePeriod, TimePeriodMicroseconds, \ TimePeriodMilliseconds, TimePeriodSeconds, TimePeriodMinutes from esphome.py_compat import integer_types, string_types, text_type, IS_PY2 @@ -25,13 +25,28 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=invalid-name Schema = _Schema -port = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)) -float_ = vol.Coerce(float) -positive_float = vol.All(float_, vol.Range(min=0)) -zero_to_one_float = vol.All(float_, vol.Range(min=0, max=1)) -negative_one_to_one_float = vol.All(float_, vol.Range(min=-1, max=1)) -positive_int = vol.All(vol.Coerce(int), vol.Range(min=0)) -positive_not_null_int = vol.All(vol.Coerce(int), vol.Range(min=0, min_included=False)) +Optional = vol.Optional +Required = vol.Required +All = vol.All +Coerce = vol.Coerce +Range = vol.Range +Invalid = vol.Invalid +MultipleInvalid = vol.MultipleInvalid +Any = vol.Any +Lower = vol.Lower +Upper = vol.Upper +Length = vol.Length +Exclusive = vol.Exclusive +Inclusive = vol.Inclusive +ALLOW_EXTRA = vol.ALLOW_EXTRA + +port = All(Coerce(int), Range(min=1, max=65535)) +float_ = Coerce(float) +positive_float = All(float_, Range(min=0)) +zero_to_one_float = All(float_, Range(min=0, max=1)) +negative_one_to_one_float = All(float_, Range(min=-1, max=1)) +positive_int = All(Coerce(int), Range(min=0)) +positive_not_null_int = All(Coerce(int), Range(min=0, min_included=False)) ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_' @@ -51,15 +66,17 @@ RESERVED_IDS = [ 'App', 'pinMode', 'delay', 'delayMicroseconds', 'digitalRead', 'digitalWrite', 'INPUT', 'OUTPUT', 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', 'int8_t', 'int16_t', 'int32_t', 'int64_t', + 'display', 'i2c', 'spi', 'uart', 'sensor', 'binary_sensor', 'climate', 'cover', 'text_sensor', + 'api', 'fan', 'light', 'gpio', 'mqtt', 'ota', 'power_supply', 'wifi' ] def alphanumeric(value): if value is None: - raise vol.Invalid("string value is None") + raise Invalid("string value is None") value = text_type(value) if not value.isalnum(): - raise vol.Invalid("string value is not alphanumeric") + raise Invalid("string value is not alphanumeric") return value @@ -67,25 +84,25 @@ def valid_name(value): value = string_strict(value) for c in value: if c not in ALLOWED_NAME_CHARS: - raise vol.Invalid(u"'{}' is an invalid character for names. Valid characters are: {}" - u" (lowercase, no spaces)".format(c, ALLOWED_NAME_CHARS)) + raise Invalid(u"'{}' is an invalid character for names. Valid characters are: {}" + u" (lowercase, no spaces)".format(c, ALLOWED_NAME_CHARS)) return value def string(value): if isinstance(value, (dict, list)): - raise vol.Invalid("string value cannot be dictionary or list.") + raise Invalid("string value cannot be dictionary or list.") if value is not None: return text_type(value) - raise vol.Invalid("string value is None") + raise Invalid("string value is None") def string_strict(value): """Strictly only allow strings.""" if isinstance(value, string_types): return value - raise vol.Invalid("Must be string, got {}. did you forget putting quotes " - "around the value?".format(type(value))) + raise Invalid("Must be string, got {}. did you forget putting quotes " + "around the value?".format(type(value))) def icon(value): @@ -93,7 +110,7 @@ def icon(value): value = string_strict(value) if value.startswith('mdi:'): return value - raise vol.Invalid('Icons should start with prefix "mdi:"') + raise Invalid('Icons should start with prefix "mdi:"') def boolean(value): @@ -104,13 +121,13 @@ def boolean(value): return True if value in ('0', 'false', 'no', 'off', 'disable'): return False - raise vol.Invalid('invalid boolean value {}'.format(value)) + raise Invalid('invalid boolean value {}'.format(value)) return bool(value) def ensure_list(*validators): """Wrap value in list if it is not one.""" - user = vol.All(*validators) + user = All(*validators) def validator(value): if value is None or (isinstance(value, dict) and not value): @@ -118,12 +135,18 @@ def ensure_list(*validators): if not isinstance(value, list): return [user(value)] ret = [] + errs = [] for i, val in enumerate(value): try: ret.append(user(val)) - except vol.Invalid as err: + except vol.MultipleInvalid as err: err.prepend([i]) - raise err + errs.extend(err.errors) + except Invalid as err: + err.prepend([i]) + errs.append(err) + if errs: + raise vol.MultipleInvalid(errs) return ret return validator @@ -139,7 +162,7 @@ def ensure_dict(value): if value is None: return {} if not isinstance(value, dict): - raise vol.Invalid("Expected a dictionary") + raise Invalid("Expected a dictionary") return value @@ -161,24 +184,24 @@ def int_(value): return int(value) -hex_int = vol.Coerce(hex_int_) +hex_int = Coerce(hex_int_) def validate_id_name(value): value = string(value) if not value: - raise vol.Invalid("ID must not be empty") + raise Invalid("ID must not be empty") if value[0].isdigit(): - raise vol.Invalid("First character in ID cannot be a digit.") + raise Invalid("First character in ID cannot be a digit.") if '-' in value: - raise vol.Invalid("Dashes are not supported in IDs, please use underscores instead.") + raise Invalid("Dashes are not supported in IDs, please use underscores instead.") for char in value: if char != '_' and not char.isalnum(): - raise vol.Invalid(u"IDs must only consist of upper/lowercase characters, the underscore" - u"character and numbers. The character '{}' cannot be used" - u"".format(char)) + raise Invalid(u"IDs must only consist of upper/lowercase characters, the underscore" + u"character and numbers. The character '{}' cannot be used" + u"".format(char)) if value in RESERVED_IDS: - raise vol.Invalid(u"ID {} is reserved internally and cannot be used".format(value)) + raise Invalid(u"ID {} is reserved internally and cannot be used".format(value)) return value @@ -219,14 +242,14 @@ def only_on(platforms): def validator_(obj): if CORE.esp_platform not in platforms: - raise vol.Invalid(u"This feature is only available on {}".format(platforms)) + raise Invalid(u"This feature is only available on {}".format(platforms)) return obj return validator_ -only_on_esp32 = only_on(ESP_PLATFORM_ESP32) -only_on_esp8266 = only_on(ESP_PLATFORM_ESP8266) +only_on_esp32 = only_on('ESP32') +only_on_esp8266 = only_on('ESP8266') # Adapted from: @@ -237,10 +260,10 @@ def has_at_least_one_key(*keys): def validate(obj): """Test keys exist in dict.""" if not isinstance(obj, dict): - raise vol.Invalid('expected dictionary') + raise Invalid('expected dictionary') if not any(k in keys for k in obj): - raise vol.Invalid('Must contain at least one of {}.'.format(', '.join(keys))) + raise Invalid('Must contain at least one of {}.'.format(', '.join(keys))) return obj return validate @@ -249,13 +272,13 @@ def has_at_least_one_key(*keys): def has_exactly_one_key(*keys): def validate(obj): if not isinstance(obj, dict): - raise vol.Invalid('expected dictionary') + raise Invalid('expected dictionary') number = sum(k in keys for k in obj) if number > 1: - raise vol.Invalid("Cannot specify more than one of {}.".format(', '.join(keys))) + raise Invalid("Cannot specify more than one of {}.".format(', '.join(keys))) if number < 1: - raise vol.Invalid('Must contain exactly one of {}.'.format(', '.join(keys))) + raise Invalid('Must contain exactly one of {}.'.format(', '.join(keys))) return obj return validate @@ -264,11 +287,11 @@ def has_exactly_one_key(*keys): def has_at_most_one_key(*keys): def validate(obj): if not isinstance(obj, dict): - raise vol.Invalid('expected dictionary') + raise Invalid('expected dictionary') number = sum(k in keys for k in obj) if number > 1: - raise vol.Invalid("Cannot specify more than one of {}.".format(', '.join(keys))) + raise Invalid("Cannot specify more than one of {}.".format(', '.join(keys))) return obj return validate @@ -276,7 +299,7 @@ def has_at_most_one_key(*keys): TIME_PERIOD_ERROR = "Time period {} should be format number + unit, for example 5ms, 5s, 5min, 5h" -time_period_dict = vol.All( +time_period_dict = All( dict, Schema({ 'days': float_, 'hours': float_, @@ -293,14 +316,14 @@ time_period_dict = vol.All( def time_period_str_colon(value): """Validate and transform time offset with format HH:MM[:SS].""" if isinstance(value, int): - raise vol.Invalid('Make sure you wrap time values in quotes') + raise Invalid('Make sure you wrap time values in quotes') if not isinstance(value, str): - raise vol.Invalid(TIME_PERIOD_ERROR.format(value)) + raise Invalid(TIME_PERIOD_ERROR.format(value)) try: parsed = [int(x) for x in value.split(':')] except ValueError: - raise vol.Invalid(TIME_PERIOD_ERROR.format(value)) + raise Invalid(TIME_PERIOD_ERROR.format(value)) if len(parsed) == 2: hour, minute = parsed @@ -308,7 +331,7 @@ def time_period_str_colon(value): elif len(parsed) == 3: hour, minute, second = parsed else: - raise vol.Invalid(TIME_PERIOD_ERROR.format(value)) + raise Invalid(TIME_PERIOD_ERROR.format(value)) return TimePeriod(hours=hour, minutes=minute, seconds=second) @@ -316,10 +339,10 @@ def time_period_str_colon(value): def time_period_str_unit(value): """Validate and transform time period with time unit and integer value.""" if isinstance(value, int): - raise vol.Invalid("Don't know what '{0}' means as it has no time *unit*! Did you mean " - "'{0}s'?".format(value)) + raise Invalid("Don't know what '{0}' means as it has no time *unit*! Did you mean " + "'{0}s'?".format(value)) if not isinstance(value, string_types): - raise vol.Invalid("Expected string for time period with unit.") + raise Invalid("Expected string for time period with unit.") unit_to_kwarg = { 'us': 'microseconds', @@ -340,8 +363,8 @@ def time_period_str_unit(value): match = re.match(r"^([-+]?[0-9]*\.?[0-9]*)\s*(\w*)$", value) if match is None: - raise vol.Invalid(u"Expected time period with unit, " - u"got {}".format(value)) + raise Invalid(u"Expected time period with unit, " + u"got {}".format(value)) kwarg = unit_to_kwarg[one_of(*unit_to_kwarg)(match.group(2))] return TimePeriod(**{kwarg: float(match.group(1))}) @@ -349,7 +372,7 @@ def time_period_str_unit(value): def time_period_in_milliseconds_(value): if value.microseconds is not None and value.microseconds != 0: - raise vol.Invalid("Maximum precision is milliseconds") + raise Invalid("Maximum precision is milliseconds") return TimePeriodMilliseconds(**value.as_dict()) @@ -359,19 +382,19 @@ def time_period_in_microseconds_(value): def time_period_in_seconds_(value): if value.microseconds is not None and value.microseconds != 0: - raise vol.Invalid("Maximum precision is seconds") + raise Invalid("Maximum precision is seconds") if value.milliseconds is not None and value.milliseconds != 0: - raise vol.Invalid("Maximum precision is seconds") + raise Invalid("Maximum precision is seconds") return TimePeriodSeconds(**value.as_dict()) def time_period_in_minutes_(value): if value.microseconds is not None and value.microseconds != 0: - raise vol.Invalid("Maximum precision is minutes") + raise Invalid("Maximum precision is minutes") if value.milliseconds is not None and value.milliseconds != 0: - raise vol.Invalid("Maximum precision is minutes") + raise Invalid("Maximum precision is minutes") if value.seconds is not None and value.seconds != 0: - raise vol.Invalid("Maximum precision is minutes") + raise Invalid("Maximum precision is minutes") return TimePeriodMinutes(**value.as_dict()) @@ -381,15 +404,15 @@ def update_interval(value): return positive_time_period_milliseconds(value) -time_period = vol.Any(time_period_str_unit, time_period_str_colon, time_period_dict) -positive_time_period = vol.All(time_period, vol.Range(min=TimePeriod())) -positive_time_period_milliseconds = vol.All(positive_time_period, time_period_in_milliseconds_) -positive_time_period_seconds = vol.All(positive_time_period, time_period_in_seconds_) -positive_time_period_minutes = vol.All(positive_time_period, time_period_in_minutes_) -time_period_microseconds = vol.All(time_period, time_period_in_microseconds_) -positive_time_period_microseconds = vol.All(positive_time_period, time_period_in_microseconds_) -positive_not_null_time_period = vol.All(time_period, - vol.Range(min=TimePeriod(), min_included=False)) +time_period = Any(time_period_str_unit, time_period_str_colon, time_period_dict) +positive_time_period = All(time_period, Range(min=TimePeriod())) +positive_time_period_milliseconds = All(positive_time_period, time_period_in_milliseconds_) +positive_time_period_seconds = All(positive_time_period, time_period_in_seconds_) +positive_time_period_minutes = All(positive_time_period, time_period_in_minutes_) +time_period_microseconds = All(time_period, time_period_in_microseconds_) +positive_time_period_microseconds = All(positive_time_period, time_period_in_microseconds_) +positive_not_null_time_period = All(time_period, + Range(min=TimePeriod(), min_included=False)) def time_of_day(value): @@ -400,7 +423,7 @@ def time_of_day(value): try: date = datetime.strptime(value, '%H:%M:%S %p') except ValueError: - raise vol.Invalid("Invalid time of day: {}".format(err)) + raise Invalid("Invalid time of day: {}".format(err)) return { CONF_HOUR: date.hour, @@ -413,21 +436,21 @@ def mac_address(value): value = string_strict(value) parts = value.split(':') if len(parts) != 6: - raise vol.Invalid("MAC Address must consist of 6 : (colon) separated parts") + raise Invalid("MAC Address must consist of 6 : (colon) separated parts") parts_int = [] if any(len(part) != 2 for part in parts): - raise vol.Invalid("MAC Address must be format XX:XX:XX:XX:XX:XX") + raise Invalid("MAC Address must be format XX:XX:XX:XX:XX:XX") for part in parts: try: parts_int.append(int(part, 16)) except ValueError: - raise vol.Invalid("MAC Address parts must be hexadecimal values from 00 to FF") + raise Invalid("MAC Address parts must be hexadecimal values from 00 to FF") return core.MACAddress(*parts_int) def uuid(value): - return vol.Coerce(uuid_.UUID)(value) + return Coerce(uuid_.UUID)(value) METRIC_SUFFIXES = { @@ -444,11 +467,11 @@ def float_with_unit(quantity, regex_suffix): match = pattern.match(string(value)) if match is None: - raise vol.Invalid(u"Expected {} with unit, got {}".format(quantity, value)) + raise Invalid(u"Expected {} with unit, got {}".format(quantity, value)) mantissa = float(match.group(1)) if match.group(2) not in METRIC_SUFFIXES: - raise vol.Invalid(u"Invalid {} suffix {}".format(quantity, match.group(2))) + raise Invalid(u"Invalid {} suffix {}".format(quantity, match.group(2))) multiplier = METRIC_SUFFIXES[match.group(2)] return mantissa * multiplier @@ -477,25 +500,25 @@ if IS_PY2: output += u' for ' + self.error_type return output + path - vol.Invalid.__unicode__ = _vol_invalid_unicode + Invalid.__unicode__ = _vol_invalid_unicode def temperature(value): try: return _temperature_c(value) - except vol.Invalid as orig_err: # noqa + except Invalid as orig_err: # noqa pass try: kelvin = _temperature_k(value) return kelvin - 273.15 - except vol.Invalid: + except Invalid: pass try: fahrenheit = _temperature_f(value) - return (fahrenheit - 32) * (5/9) - except vol.Invalid: + return (fahrenheit - 32) * (5 / 9) + except Invalid: pass raise orig_err # noqa @@ -508,10 +531,10 @@ _color_temperature_kelvin = float_with_unit('Color Temperature', r'(K|Kelvin)') def color_temperature(value): try: val = _color_temperature_mireds(value) - except vol.Invalid: + except Invalid: val = 1000000.0 / _color_temperature_kelvin(value) if val < 0: - raise vol.Invalid("Color temperature cannot be negative") + raise Invalid("Color temperature cannot be negative") return val @@ -520,25 +543,25 @@ def validate_bytes(value): match = re.match(r"^([0-9]+)\s*(\w*?)(?:byte|B|b)?s?$", value) if match is None: - raise vol.Invalid(u"Expected number of bytes with unit, got {}".format(value)) + raise Invalid(u"Expected number of bytes with unit, got {}".format(value)) mantissa = int(match.group(1)) if match.group(2) not in METRIC_SUFFIXES: - raise vol.Invalid(u"Invalid metric suffix {}".format(match.group(2))) + raise Invalid(u"Invalid metric suffix {}".format(match.group(2))) multiplier = METRIC_SUFFIXES[match.group(2)] if multiplier < 1: - raise vol.Invalid(u"Only suffixes with positive exponents are supported. " - u"Got {}".format(match.group(2))) + raise Invalid(u"Only suffixes with positive exponents are supported. " + u"Got {}".format(match.group(2))) return int(mantissa * multiplier) def hostname(value): value = string(value) if len(value) > 63: - raise vol.Invalid("Hostnames can only be 63 characters long") + raise Invalid("Hostnames can only be 63 characters long") for c in value: if not (c.isalnum() or c in '_-'): - raise vol.Invalid("Hostname can only have alphanumeric characters and _ or -") + raise Invalid("Hostname can only have alphanumeric characters and _ or -") return value @@ -548,8 +571,8 @@ def domain(value): return value try: return str(ipv4(value)) - except vol.Invalid: - raise vol.Invalid("Invalid domain: {}".format(value)) + except Invalid: + raise Invalid("Invalid domain: {}".format(value)) def domain_name(value): @@ -557,21 +580,21 @@ def domain_name(value): if not value: return value if not value.startswith('.'): - raise vol.Invalid("Domain name must start with .") + raise Invalid("Domain name must start with .") if value.startswith('..'): - raise vol.Invalid("Domain name must start with single .") + raise Invalid("Domain name must start with single .") for c in value: if not (c.isalnum() or c in '._-'): - raise vol.Invalid("Domain name can only have alphanumeric characters and _ or -") + raise Invalid("Domain name can only have alphanumeric characters and _ or -") return value def ssid(value): value = string_strict(value) if not value: - raise vol.Invalid("SSID can't be empty.") + raise Invalid("SSID can't be empty.") if len(value) > 32: - raise vol.Invalid("SSID can't be longer than 32 characters") + raise Invalid("SSID can't be longer than 32 characters") return value @@ -583,34 +606,34 @@ def ipv4(value): elif isinstance(value, IPAddress): return value else: - raise vol.Invalid("IPv4 address must consist of either string or " - "integer list") + raise Invalid("IPv4 address must consist of either string or " + "integer list") if len(parts) != 4: - raise vol.Invalid("IPv4 address must consist of four point-separated " - "integers") + raise Invalid("IPv4 address must consist of four point-separated " + "integers") parts_ = list(map(int, parts)) if not all(0 <= x < 256 for x in parts_): - raise vol.Invalid("IPv4 address parts must be in range from 0 to 255") + raise Invalid("IPv4 address parts must be in range from 0 to 255") return IPAddress(*parts_) def _valid_topic(value): """Validate that this is a valid topic name/filter.""" if isinstance(value, dict): - raise vol.Invalid("Can't use dictionary with topic") + raise Invalid("Can't use dictionary with topic") value = string(value) try: raw_value = value.encode('utf-8') except UnicodeError: - raise vol.Invalid("MQTT topic name/filter must be valid UTF-8 string.") + raise Invalid("MQTT topic name/filter must be valid UTF-8 string.") if not raw_value: - raise vol.Invalid("MQTT topic name/filter must not be empty.") + raise Invalid("MQTT topic name/filter must not be empty.") if len(raw_value) > 65535: - raise vol.Invalid("MQTT topic name/filter must not be longer than " - "65535 encoded bytes.") + raise Invalid("MQTT topic name/filter must not be longer than " + "65535 encoded bytes.") if '\0' in value: - raise vol.Invalid("MQTT topic name/filter must not contain null " - "character.") + raise Invalid("MQTT topic name/filter must not contain null " + "character.") return value @@ -620,18 +643,18 @@ def subscribe_topic(value): for i in (i for i, c in enumerate(value) if c == '+'): if (i > 0 and value[i - 1] != '/') or \ (i < len(value) - 1 and value[i + 1] != '/'): - raise vol.Invalid("Single-level wildcard must occupy an entire " - "level of the filter") + raise Invalid("Single-level wildcard must occupy an entire " + "level of the filter") index = value.find('#') if index != -1: if index != len(value) - 1: # If there are multiple wildcards, this will also trigger - raise vol.Invalid("Multi-level wildcard must be the last " - "character in the topic filter.") + raise Invalid("Multi-level wildcard must be the last " + "character in the topic filter.") if len(value) > 1 and value[index - 1] != '/': - raise vol.Invalid("Multi-level wildcard must be after a topic " - "level separator.") + raise Invalid("Multi-level wildcard must be after a topic " + "level separator.") return value @@ -640,7 +663,7 @@ def publish_topic(value): """Validate that we can publish using this MQTT topic.""" value = _valid_topic(value) if '+' in value or '#' in value: - raise vol.Invalid("Wildcards can not be used in topic names") + raise Invalid("Wildcards can not be used in topic names") return value @@ -654,25 +677,25 @@ def mqtt_qos(value): try: value = int(value) except (TypeError, ValueError): - raise vol.Invalid(u"MQTT Quality of Service must be integer, got {}".format(value)) + raise Invalid(u"MQTT Quality of Service must be integer, got {}".format(value)) return one_of(0, 1, 2)(value) def requires_component(comp): def validator(value): if comp not in CORE.raw_config: - raise vol.Invalid("This option requires component {}".format(comp)) + raise Invalid("This option requires component {}".format(comp)) return value return validator -uint8_t = vol.All(int_, vol.Range(min=0, max=255)) -uint16_t = vol.All(int_, vol.Range(min=0, max=65535)) -uint32_t = vol.All(int_, vol.Range(min=0, max=4294967295)) -hex_uint8_t = vol.All(hex_int, vol.Range(min=0, max=255)) -hex_uint16_t = vol.All(hex_int, vol.Range(min=0, max=65535)) -hex_uint32_t = vol.All(hex_int, vol.Range(min=0, max=4294967295)) +uint8_t = All(int_, Range(min=0, max=255)) +uint16_t = All(int_, Range(min=0, max=65535)) +uint32_t = All(int_, Range(min=0, max=4294967295)) +hex_uint8_t = All(hex_int, Range(min=0, max=255)) +hex_uint16_t = All(hex_int, Range(min=0, max=65535)) +hex_uint32_t = All(hex_int, Range(min=0, max=4294967295)) i2c_address = hex_uint8_t @@ -689,12 +712,12 @@ def possibly_negative_percentage(value): msg = "Percentage must not be higher than 100%." if not has_percent_sign: msg += " Please put a percent sign after the number!" - raise vol.Invalid(msg) + raise Invalid(msg) if value < -1: msg = "Percentage must not be smaller than -100%." if not has_percent_sign: msg += " Please put a percent sign after the number!" - raise vol.Invalid(msg) + raise Invalid(msg) return negative_one_to_one_float(value) @@ -706,7 +729,7 @@ def percentage_int(value): def invalid(message): def validator(value): - raise vol.Invalid(message) + raise Invalid(message) return validator @@ -721,6 +744,7 @@ def one_of(*values, **kwargs): upper = kwargs.get('upper', False) string_ = kwargs.get('string', False) or lower or upper to_int = kwargs.get('int', False) + to_float = kwargs.get('float', False) space = kwargs.get('space', ' ') def validator(value): @@ -729,12 +753,14 @@ def one_of(*values, **kwargs): value = value.replace(' ', space) if to_int: value = int_(value) + if to_float: + value = float_(value) if lower: - value = vol.Lower(value) + value = Lower(value) if upper: - value = vol.Upper(value) + value = Upper(value) if value not in values: - raise vol.Invalid(u"Unknown value '{}', must be one of {}".format(value, options)) + raise Invalid(u"Unknown value '{}', must be one of {}".format(value, options)) return value return validator @@ -749,18 +775,18 @@ def lambda_(value): def dimensions(value): if isinstance(value, list): if len(value) != 2: - raise vol.Invalid(u"Dimensions must have a length of two, not {}".format(len(value))) + raise Invalid(u"Dimensions must have a length of two, not {}".format(len(value))) try: width, height = int(value[0]), int(value[1]) except ValueError: - raise vol.Invalid(u"Width and height dimensions must be integers") + raise Invalid(u"Width and height dimensions must be integers") if width <= 0 or height <= 0: - raise vol.Invalid(u"Width and height must at least be 1") + raise Invalid(u"Width and height must at least be 1") return [width, height] value = string(value) match = re.match(r"\s*([0-9]+)\s*[xX]\s*([0-9]+)\s*", value) if not match: - raise vol.Invalid(u"Invalid value '{}' for dimensions. Only WIDTHxHEIGHT is allowed.") + raise Invalid(u"Invalid value '{}' for dimensions. Only WIDTHxHEIGHT is allowed.") return dimensions([match.group(1), match.group(2)]) @@ -768,10 +794,10 @@ def directory(value): value = string(value) path = CORE.relative_path(value) if not os.path.exists(path): - raise vol.Invalid(u"Could not find directory '{}'. Please make sure it exists.".format( + raise Invalid(u"Could not find directory '{}'. Please make sure it exists.".format( path)) if not os.path.isdir(path): - raise vol.Invalid(u"Path '{}' is not a directory.".format(path)) + raise Invalid(u"Path '{}' is not a directory.".format(path)) return value @@ -779,10 +805,10 @@ def file_(value): value = string(value) path = CORE.relative_path(value) if not os.path.exists(path): - raise vol.Invalid(u"Could not find file '{}'. Please make sure it exists.".format( + raise Invalid(u"Could not find file '{}'. Please make sure it exists.".format( path)) if not os.path.isfile(path): - raise vol.Invalid(u"Path '{}' is not a file.".format(path)) + raise Invalid(u"Path '{}' is not a file.".format(path)) return value @@ -792,28 +818,77 @@ ENTITY_ID_CHARACTERS = 'abcdefghijklmnopqrstuvwxyz0123456789_' def entity_id(value): value = string_strict(value).lower() if value.count('.') != 1: - raise vol.Invalid("Entity ID must have exactly one dot in it") + raise Invalid("Entity ID must have exactly one dot in it") for x in value.split('.'): for c in x: if c not in ENTITY_ID_CHARACTERS: - raise vol.Invalid("Invalid character for entity ID: {}".format(c)) + raise Invalid("Invalid character for entity ID: {}".format(c)) return value -class GenerateID(vol.Optional): +def extract_keys(schema): + if isinstance(schema, Schema): + schema = schema.schema + assert isinstance(schema, dict) + keys = list(schema.keys()) + keys.sort() + return keys + + +class GenerateID(Optional): def __init__(self, key=CONF_ID): super(GenerateID, self).__init__(key, default=lambda: None) +class SplitDefault(Optional): + def __init__(self, key, esp8266=vol.UNDEFINED, esp32=vol.UNDEFINED): + super(SplitDefault, self).__init__(key) + self._esp8266_default = vol.default_factory(esp8266) + self._esp32_default = vol.default_factory(esp32) + + @property + def default(self): + if CORE.is_esp8266: + return self._esp8266_default + if CORE.is_esp32: + return self._esp32_default + raise ValueError + + @default.setter + def default(self, value): + # Ignore default set from vol.Optional + pass + + +class OnlyWith(Optional): + def __init__(self, key, component, default=None): + super(OnlyWith, self).__init__(key) + self._component = component + self._default = vol.default_factory(default) + + @property + def default(self): + if self._component not in CORE.raw_config: + return vol.UNDEFINED + return self._default + + @default.setter + def default(self, value): + # Ignore default set from vol.Optional + pass + + def nameable(*schemas): def validator(config): - config = vol.All(*schemas)(config) + config = All(*schemas)(config) if CONF_NAME not in config and CONF_ID not in config: - raise vol.Invalid("At least one of 'id:' or 'name:' is required!") + print(config) + raise Invalid("At least one of 'id:' or 'name:' is required!") if CONF_NAME not in config: id = config[CONF_ID] if not id.is_manual: - raise vol.Invalid("At least one of 'id:' or 'name:' is required!") + print(config) + raise Invalid("At least one of 'id:' or 'name:' is required!") config[CONF_NAME] = id.id config[CONF_INTERNAL] = True return config @@ -822,30 +897,70 @@ def nameable(*schemas): return validator -PLATFORM_SCHEMA = Schema({ - vol.Required(CONF_PLATFORM): valid, -}) +def validate_registry_entry(name, registry, ignore_keys): + def validator(value): + if isinstance(value, string_types): + value = {value: {}} + if not isinstance(value, dict): + raise Invalid(u"{} must consist of key-value mapping! Got {}" + u"".format(name.title(), value)) + item = value.copy() + key = next((x for x in item if x not in ignore_keys), None) + if key is None: + raise Invalid(u"Key missing from {}! Got {}".format(name, item)) + if key not in registry: + raise vol.Invalid(u"Unable to find {} with the name '{}'".format(name, key)) + key2 = next((x for x in item if x != key and x not in ignore_keys), None) + if key2 is not None: + raise vol.Invalid(u"Cannot have two {0}s in one item. Key '{1}' overrides '{2}'! " + u"Did you forget to indent the block inside the {0}?" + u"".format(name, key, key2)) + validator_ = registry[key][0] + try: + item[key] = validator_(item[key] or {}) + except vol.Invalid as err: + err.prepend([key]) + raise err + return item + + return validator + + +def validate_registry(name, registry, ignore_keys): + return ensure_list(validate_registry_entry(name, registry, ignore_keys)) + + +def maybe_simple_value(*validators): + validator = All(*validators) + + def validate(value): + if isinstance(value, dict) and CONF_VALUE in value: + return validator(value) + return validator({CONF_VALUE: value}) + + return validate + MQTT_COMPONENT_AVAILABILITY_SCHEMA = Schema({ - vol.Required(CONF_TOPIC): subscribe_topic, - vol.Optional(CONF_PAYLOAD_AVAILABLE, default='online'): mqtt_payload, - vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE, default='offline'): mqtt_payload, + Required(CONF_TOPIC): subscribe_topic, + Optional(CONF_PAYLOAD_AVAILABLE, default='online'): mqtt_payload, + Optional(CONF_PAYLOAD_NOT_AVAILABLE, default='offline'): mqtt_payload, }) MQTT_COMPONENT_SCHEMA = Schema({ - vol.Optional(CONF_NAME): string, - vol.Optional(CONF_RETAIN): vol.All(requires_component('mqtt'), boolean), - vol.Optional(CONF_DISCOVERY): vol.All(requires_component('mqtt'), boolean), - vol.Optional(CONF_STATE_TOPIC): vol.All(requires_component('mqtt'), publish_topic), - vol.Optional(CONF_AVAILABILITY): vol.All(requires_component('mqtt'), - vol.Any(None, MQTT_COMPONENT_AVAILABILITY_SCHEMA)), - vol.Optional(CONF_INTERNAL): boolean, + Optional(CONF_NAME): string, + Optional(CONF_RETAIN): All(requires_component('mqtt'), boolean), + Optional(CONF_DISCOVERY): All(requires_component('mqtt'), boolean), + Optional(CONF_STATE_TOPIC): All(requires_component('mqtt'), publish_topic), + Optional(CONF_AVAILABILITY): All(requires_component('mqtt'), + Any(None, MQTT_COMPONENT_AVAILABILITY_SCHEMA)), + Optional(CONF_INTERNAL): boolean, }) MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend({ - vol.Optional(CONF_COMMAND_TOPIC): vol.All(requires_component('mqtt'), subscribe_topic), + Optional(CONF_COMMAND_TOPIC): All(requires_component('mqtt'), subscribe_topic), }) COMPONENT_SCHEMA = Schema({ - vol.Optional(CONF_SETUP_PRIORITY): float_ + Optional(CONF_SETUP_PRIORITY): float_ }) diff --git a/esphome/const.py b/esphome/const.py index 840d4c53b..5860a724b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,3 +1,4 @@ +# coding=utf-8 """Constants used by esphome.""" MAJOR_VERSION = 1 @@ -5,7 +6,6 @@ MINOR_VERSION = 13 PATCH_VERSION = '0-dev' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) -ESPHOME_CORE_VERSION = 'dev' ESP_PLATFORM_ESP32 = 'ESP32' ESP_PLATFORM_ESP8266 = 'ESP8266' @@ -13,446 +13,6 @@ ESP_PLATFORMS = [ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266] APB_CLOCK_FREQ = 80000000 -CONF_ESPHOME = 'esphome' -CONF_NAME = 'name' -CONF_PLATFORM = 'platform' -CONF_BOARD = 'board' -CONF_ESPHOME_CORE_VERSION = 'esphome_core_version' -CONF_USE_CUSTOM_CODE = 'use_custom_code' -CONF_ARDUINO_VERSION = 'arduino_version' -CONF_LOCAL = 'local' -CONF_REPOSITORY = 'repository' -CONF_COMMIT = 'commit' -CONF_SERVICES = 'services' -CONF_TAG = 'tag' -CONF_BRANCH = 'branch' -CONF_LOGGER = 'logger' -CONF_WIFI = 'wifi' -CONF_SSID = 'ssid' -CONF_IP_ADDRESS = 'ip_address' -CONF_BSSID = 'bssid' -CONF_PASSWORD = 'password' -CONF_MANUAL_IP = 'manual_ip' -CONF_STATIC_IP = 'static_ip' -CONF_GATEWAY = 'gateway' -CONF_SUBNET = 'subnet' -CONF_OTA = 'ota' -CONF_MQTT = 'mqtt' -CONF_BROKER = 'broker' -CONF_USERNAME = 'username' -CONF_MIN_LEVEL = 'min_level' -CONF_IDLE_LEVEL = 'idle_level' -CONF_MAX_LEVEL = 'max_level' -CONF_POWER_SUPPLY = 'power_supply' -CONF_ID = 'id' -CONF_MQTT_ID = 'mqtt_id' -CONF_SENSOR_ID = 'sensor_id' -CONF_TRIGGER_ID = 'trigger_id' -CONF_ACTION_ID = 'action_id' -CONF_CONDITION_ID = 'condition_id' -CONF_MAKE_ID = 'make_id' -CONF_AUTOMATION_ID = 'automation_id' -CONF_DELAY = 'delay' -CONF_PIN = 'pin' -CONF_NUMBER = 'number' -CONF_INVERTED = 'inverted' -CONF_I2C = 'i2c' -CONF_SDA = 'sda' -CONF_SCL = 'scl' -CONF_FREQUENCY = 'frequency' -CONF_PCA9685 = 'pca9685' -CONF_PCA9685_ID = 'pca9685_id' -CONF_OUTPUT = 'output' -CONF_CHANNEL = 'channel' -CONF_CHANNELS = 'channels' -CONF_LIGHT = 'light' -CONF_RED = 'red' -CONF_GREEN = 'green' -CONF_BLUE = 'blue' -CONF_SENSOR = 'sensor' -CONF_TEMPERATURE = 'temperature' -CONF_HUMIDITY = 'humidity' -CONF_MODEL = 'model' -CONF_BINARY_SENSOR = 'binary_sensor' -CONF_DEVICE_CLASS = 'device_class' -CONF_GPIO = 'gpio' -CONF_DHT = 'dht' -CONF_SAFE_MODE = 'safe_mode' -CONF_MODE = 'mode' -CONF_GAMMA_CORRECT = 'gamma_correct' -CONF_RETAIN = 'retain' -CONF_DISCOVERY = 'discovery' -CONF_DISCOVERY_PREFIX = 'discovery_prefix' -CONF_STATE_TOPIC = 'state_topic' -CONF_COMMAND_TOPIC = 'command_topic' -CONF_AVAILABILITY = 'availability' -CONF_TOPIC = 'topic' -CONF_PAYLOAD_AVAILABLE = 'payload_available' -CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' -CONF_DEFAULT_TRANSITION_LENGTH = 'default_transition_length' -CONF_TRANSITION_LENGTH = 'transition_length' -CONF_FLASH_LENGTH = 'flash_length' -CONF_BRIGHTNESS = 'brightness' -CONF_EFFECT = 'effect' -CONF_ABOVE = 'above' -CONF_BELOW = 'below' -CONF_ON = 'on' -CONF_IF = 'if' -CONF_WHILE = 'while' -CONF_WAIT_UNTIL = 'wait_until' -CONF_THEN = 'then' -CONF_BINARY = 'binary' -CONF_WHITE = 'white' -CONF_RGBW = 'rgbw' -CONF_MAX_POWER = 'max_power' -CONF_BIT_DEPTH = 'bit_depth' -CONF_BAUD_RATE = 'baud_rate' -CONF_LOG_TOPIC = 'log_topic' -CONF_TX_BUFFER_SIZE = 'tx_buffer_size' -CONF_LEVEL = 'level' -CONF_LOGS = 'logs' -CONF_PORT = 'port' -CONF_WILL_MESSAGE = 'will_message' -CONF_BIRTH_MESSAGE = 'birth_message' -CONF_SHUTDOWN_MESSAGE = 'shutdown_message' -CONF_PAYLOAD = 'payload' -CONF_QOS = 'qos' -CONF_DISCOVERY_RETAIN = 'discovery_retain' -CONF_TOPIC_PREFIX = 'topic_prefix' -CONF_PHASE_BALANCER = 'phase_balancer' -CONF_ADDRESS = 'address' -CONF_ENABLE_TIME = 'enable_time' -CONF_KEEP_ON_TIME = 'keep_on_time' -CONF_DNS1 = 'dns1' -CONF_DNS2 = 'dns2' -CONF_UNIT_OF_MEASUREMENT = 'unit_of_measurement' -CONF_ICON = 'icon' -CONF_ACCURACY_DECIMALS = 'accuracy_decimals' -CONF_EXPIRE_AFTER = 'expire_after' -CONF_FILTERS = 'filters' -CONF_OFFSET = 'offset' -CONF_MULTIPLY = 'multiply' -CONF_FILTER_OUT = 'filter_out' -CONF_SLIDING_WINDOW_MOVING_AVERAGE = 'sliding_window_moving_average' -CONF_EXPONENTIAL_MOVING_AVERAGE = 'exponential_moving_average' -CONF_WINDOW_SIZE = 'window_size' -CONF_SEND_EVERY = 'send_every' -CONF_ALPHA = 'alpha' -CONF_LAMBDA = 'lambda' -CONF_THROTTLE = 'throttle' -CONF_DELTA = 'delta' -CONF_OR = 'or' -CONF_CALIBRATE_LINEAR = 'calibrate_linear' -CONF_AND = 'and' -CONF_RANGE = 'range' -CONF_UNIQUE = 'unique' -CONF_HEARTBEAT = 'heartbeat' -CONF_DEBOUNCE = 'debounce' -CONF_UPDATE_INTERVAL = 'update_interval' -CONF_PULL_MODE = 'pull_mode' -CONF_COUNT_MODE = 'count_mode' -CONF_RISING_EDGE = 'rising_edge' -CONF_FALLING_EDGE = 'falling_edge' -CONF_INTERNAL_FILTER = 'internal_filter' -CONF_DALLAS_ID = 'dallas_id' -CONF_INDEX = 'index' -CONF_RESOLUTION = 'resolution' -CONF_ATTENUATION = 'attenuation' -CONF_PRESSURE = 'pressure' -CONF_TRIGGER_PIN = 'trigger_pin' -CONF_ECHO_PIN = 'echo_pin' -CONF_TIMEOUT = 'timeout' -CONF_CARRIER_DUTY_PERCENT = 'carrier_duty_percent' -CONF_NEC = 'nec' -CONF_COMMAND = 'command' -CONF_DATA = 'data' -CONF_NBITS = 'nbits' -CONF_JVC = 'jvc' -CONF_RC5 = 'rc5' -CONF_LG = 'lg' -CONF_SAMSUNG = 'samsung' -CONF_SONY = 'sony' -CONF_PANASONIC = 'panasonic' -CONF_REPEAT = 'repeat' -CONF_TIMES = 'times' -CONF_WAIT_TIME = 'wait_time' -CONF_OSCILLATION_OUTPUT = 'oscillation_output' -CONF_SPEED = 'speed' -CONF_OSCILLATION_STATE_TOPIC = 'oscillation_state_topic' -CONF_OSCILLATION_COMMAND_TOPIC = 'oscillation_command_topic' -CONF_OSCILLATING = 'oscillating' -CONF_SPEED_STATE_TOPIC = 'speed_state_topic' -CONF_SPEED_COMMAND_TOPIC = 'speed_command_topic' -CONF_LOW = 'low' -CONF_MEDIUM = 'medium' -CONF_HIGH = 'high' -CONF_NUM_ATTEMPTS = 'num_attempts' -CONF_CLIENT_ID = 'client_id' -CONF_RAW = 'raw' -CONF_CARRIER_FREQUENCY = 'carrier_frequency' -CONF_RATE = 'rate' -CONF_ADS1115_ID = 'ads1115_id' -CONF_MULTIPLEXER = 'multiplexer' -CONF_GAIN = 'gain' -CONF_SLEEP_DURATION = 'sleep_duration' -CONF_WAKEUP_PIN = 'wakeup_pin' -CONF_RUN_CYCLES = 'run_cycles' -CONF_RUN_DURATION = 'run_duration' -CONF_AP = 'ap' -CONF_CSS_URL = 'css_url' -CONF_JS_URL = 'js_url' -CONF_SSL_FINGERPRINTS = 'ssl_fingerprints' -CONF_PCF8574 = 'pcf8574' -CONF_MCP23017 = 'mcp23017' -CONF_PCF8575 = 'pcf8575' -CONF_SCAN = 'scan' -CONF_KEEPALIVE = 'keepalive' -CONF_INTEGRATION_TIME = 'integration_time' -CONF_RECEIVE_TIMEOUT = 'receive_timeout' -CONF_SCAN_INTERVAL = 'scan_interval' -CONF_MAC_ADDRESS = 'mac_address' -CONF_SETUP_MODE = 'setup_mode' -CONF_SETUP_PRIORITY = 'setup_priority' -CONF_IIR_FILTER = 'iir_filter' -CONF_MEASUREMENT_DURATION = 'measurement_duration' -CONF_LOW_VOLTAGE_REFERENCE = 'low_voltage_reference' -CONF_HIGH_VOLTAGE_REFERENCE = 'high_voltage_reference' -CONF_VOLTAGE_ATTENUATION = 'voltage_attenuation' -CONF_THRESHOLD = 'threshold' -CONF_OVERSAMPLING = 'oversampling' -CONF_GAS_RESISTANCE = 'gas_resistance' -CONF_NUM_LEDS = 'num_leds' -CONF_MAX_REFRESH_RATE = 'max_refresh_rate' -CONF_CHIPSET = 'chipset' -CONF_DATA_PIN = 'data_pin' -CONF_CLOCK_PIN = 'clock_pin' -CONF_RGB_ORDER = 'rgb_order' -CONF_ACCURACY = 'accuracy' -CONF_BOARD_FLASH_MODE = 'board_flash_mode' -CONF_ON_PRESS = 'on_press' -CONF_ON_RELEASE = 'on_release' -CONF_ON_STATE = 'on_state' -CONF_ON_CLICK = 'on_click' -CONF_ON_DOUBLE_CLICK = 'on_double_click' -CONF_ON_MULTI_CLICK = 'on_multi_click' -CONF_MIN_LENGTH = 'min_length' -CONF_MAX_LENGTH = 'max_length' -CONF_ON_VALUE = 'on_value' -CONF_ON_RAW_VALUE = 'on_raw_value' -CONF_ON_VALUE_RANGE = 'on_value_range' -CONF_ON_MESSAGE = 'on_message' -CONF_CS_PIN = 'cs_pin' -CONF_CLK_PIN = 'clk_pin' -CONF_MISO_PIN = 'miso_pin' -CONF_MOSI_PIN = 'mosi_pin' -CONF_TURN_ON_ACTION = 'turn_on_action' -CONF_TURN_OFF_ACTION = 'turn_off_action' -CONF_OPEN_ACTION = 'open_action' -CONF_CLOSE_ACTION = 'close_action' -CONF_STOP_ACTION = 'stop_action' -CONF_DOMAIN = 'domain' -CONF_OPTIMISTIC = 'optimistic' -CONF_ASSUMED_STATE = 'assumed_state' -CONF_ON_BOOT = 'on_boot' -CONF_ON_SHUTDOWN = 'on_shutdown' -CONF_PRIORITY = 'priority' -CONF_DUMP = 'dump' -CONF_BUFFER_SIZE = 'buffer_size' -CONF_TOLERANCE = 'tolerance' -CONF_FILTER = 'filter' -CONF_IDLE = 'idle' -CONF_NETWORKS = 'networks' -CONF_INTERNAL = 'internal' -CONF_BUILD_PATH = 'build_path' -CONF_PLATFORMIO_OPTIONS = 'platformio_options' -CONF_REBOOT_TIMEOUT = 'reboot_timeout' -CONF_STOP = 'stop' -CONF_POSITION = 'position' -CONF_TILT = 'tilt' -CONF_INVERT = 'invert' -CONF_DELAYED_ON = 'delayed_on' -CONF_DELAYED_OFF = 'delayed_off' -CONF_UUID = 'uuid' -CONF_TYPE = 'type' -CONF_SPI_ID = 'spi_id' -CONF_HARDWARE_UART = 'hardware_uart' -CONF_UART_ID = 'uart_id' -CONF_UID = 'uid' -CONF_TX_PIN = 'tx_pin' -CONF_RX_PIN = 'rx_pin' -CONF_CO2 = 'co2' -CONF_SHUNT_RESISTANCE = 'shunt_resistance' -CONF_MAX_CURRENT = 'max_current' -CONF_MAX_VOLTAGE = 'max_voltage' -CONF_CURRENT = 'current' -CONF_POWER = 'power' -CONF_BUS_VOLTAGE = 'bus_voltage' -CONF_SHUNT_VOLTAGE = 'shunt_voltage' -CONF_CONDITION = 'condition' -CONF_ELSE = 'else' -CONF_EFFECTS = 'effects' -CONF_RANDOM = 'random' -CONF_EFFECT_ID = 'effect_id' -CONF_COLORS = 'colors' -CONF_STATE = 'state' -CONF_DURATION = 'duration' -CONF_WIDTH = 'width' -CONF_ILLUMINANCE = 'illuminance' -CONF_COLOR_TEMPERATURE = 'color_temperature' -CONF_BATTERY_LEVEL = 'battery_level' -CONF_MOISTURE = 'moisture' -CONF_CONDUCTIVITY = 'conductivity' -CONF_OPEN_ENDSTOP = 'open_endstop' -CONF_OPEN_DURATION = 'open_duration' -CONF_CLOSE_ENDSTOP = 'close_endstop' -CONF_CLOSE_DURATION = 'close_duration' -CONF_MAX_DURATION = 'max_duration' -CONF_RC_SWITCH_RAW = 'rc_switch_raw' -CONF_CURRENT_OPERATION = 'current_operation' -CONF_RC_SWITCH_TYPE_A = 'rc_switch_type_a' -CONF_RC_SWITCH_TYPE_B = 'rc_switch_type_b' -CONF_RC_SWITCH_TYPE_C = 'rc_switch_type_c' -CONF_RC_SWITCH_TYPE_D = 'rc_switch_type_d' -CONF_CODE = 'code' -CONF_PROTOCOL = 'protocol' -CONF_PULSE_LENGTH = 'pulse_length' -CONF_SYNC = 'sync' -CONF_ZERO = 'zero' -CONF_ONE = 'one' -CONF_GROUP = 'group' -CONF_DEVICE = 'device' -CONF_FAMILY = 'family' -CONF_FILE = 'file' -CONF_GLYPHS = 'glyphs' -CONF_SIZE = 'size' -CONF_RESIZE = 'resize' -CONF_ROTATION = 'rotation' -CONF_DC_PIN = 'dc_pin' -CONF_RESET_PIN = 'reset_pin' -CONF_BUSY_PIN = 'busy_pin' -CONF_ESP8266_RESTORE_FROM_FLASH = 'esp8266_restore_from_flash' -CONF_FULL_UPDATE_EVERY = 'full_update_every' -CONF_DATA_PINS = 'data_pins' -CONF_ENABLE_PIN = 'enable_pin' -CONF_RS_PIN = 'rs_pin' -CONF_RW_PIN = 'rw_pin' -CONF_DIMENSIONS = 'dimensions' -CONF_NUM_CHIPS = 'num_chips' -CONF_INTENSITY = 'intensity' -CONF_EXTERNAL_VCC = 'external_vcc' -CONF_TIMEZONE = 'timezone' -CONF_SERVERS = 'servers' -CONF_HEATER = 'heater' -CONF_VOLTAGE = 'voltage' -CONF_CURRENT_RESISTOR = 'current_resistor' -CONF_VOLTAGE_DIVIDER = 'voltage_divider' -CONF_SEL_PIN = 'sel_pin' -CONF_CF_PIN = 'cf_pin' -CONF_CF1_PIN = 'cf1_pin' -CONF_CHANGE_MODE_EVERY = 'change_mode_every' -CONF_PAGE_ID = 'page_id' -CONF_COMPONENT_ID = 'component_id' -CONF_COLD_WHITE = 'cold_white' -CONF_PAGES = 'pages' -CONF_WARM_WHITE = 'warm_white' -CONF_COLD_WHITE_COLOR_TEMPERATURE = 'cold_white_color_temperature' -CONF_WARM_WHITE_COLOR_TEMPERATURE = 'warm_white_color_temperature' -CONF_HIDDEN = 'hidden' -CONF_ON_LOOP = 'on_loop' -CONF_ON_TIME = 'on_time' -CONF_SECONDS = 'seconds' -CONF_SECOND = 'second' -CONF_MINUTES = 'minutes' -CONF_MINUTE = 'minute' -CONF_HOURS = 'hours' -CONF_HOUR = 'hour' -CONF_DAYS_OF_MONTH = 'days_of_month' -CONF_MONTHS = 'months' -CONF_DAYS_OF_WEEK = 'days_of_week' -CONF_CRON = 'cron' -CONF_POWER_SAVE_MODE = 'power_save_mode' -CONF_POWER_ON_VALUE = 'power_on_value' -CONF_PM_1_0 = 'pm_1_0' -CONF_PM_2_5 = 'pm_2_5' -CONF_PM_10_0 = 'pm_10_0' -CONF_FORMALDEHYDE = 'formaldehyde' -CONF_ON_TAG = 'on_tag' -CONF_ARGS = 'args' -CONF_FORMAT = 'format' -CONF_FOR = 'for' -CONF_COLOR_CORRECT = 'color_correct' -CONF_ON_JSON_MESSAGE = 'on_json_message' -CONF_ACCELERATION = 'acceleration' -CONF_DECELERATION = 'deceleration' -CONF_MAX_SPEED = 'max_speed' -CONF_TARGET = 'target' -CONF_POSITION = 'position' -CONF_STEP_PIN = 'step_pin' -CONF_DIR_PIN = 'dir_pin' -CONF_SLEEP_PIN = 'sleep_pin' -CONF_SEND_FIRST_AT = 'send_first_at' -CONF_TIME_ID = 'time_id' -CONF_AT = 'at' -CONF_RESTORE_STATE = 'restore_state' -CONF_TIMING = 'timing' -CONF_INVALID_COOLDOWN = 'invalid_cooldown' -CONF_MY9231_ID = 'my9231_id' -CONF_NUM_CHANNELS = 'num_channels' -CONF_UPDATE_ON_BOOT = 'update_on_boot' -CONF_INITIAL_VALUE = 'initial_value' -CONF_RESTORE_VALUE = 'restore_value' -CONF_PINS = 'pins' -CONF_SENSORS = 'sensors' -CONF_BINARY_SENSORS = 'binary_sensors' -CONF_AWAY = 'away' -CONF_MIN_TEMPERATURE = 'min_temperature' -CONF_MAX_TEMPERATURE = 'max_temperature' -CONF_TARGET_TEMPERATURE = 'target_temperature' -CONF_TARGET_TEMPERATURE_LOW = 'target_temperature_low' -CONF_TARGET_TEMPERATURE_HIGH = 'target_temperature_high' -CONF_TEMPERATURE_STEP = 'temperature_step' -CONF_VISUAL = 'visual' -CONF_AWAY_CONFIG = 'away_config' -CONF_COOL_ACTION = 'cool_action' -CONF_HEAT_ACTION = 'heat_action' -CONF_IDLE_ACTION = 'idle_action' -CONF_DEFAULT_TARGET_TEMPERATURE_HIGH = 'default_target_temperature_high' -CONF_DEFAULT_TARGET_TEMPERATURE_LOW = 'default_target_temperature_low' -CONF_OUTPUTS = 'outputs' -CONF_SWITCHES = 'switches' -CONF_TEXT_SENSORS = 'text_sensors' -CONF_INCLUDES = 'includes' -CONF_LIBRARIES = 'libraries' -CONF_PIN_A = 'pin_a' -CONF_PIN_B = 'pin_b' -CONF_PIN_C = 'pin_c' -CONF_PIN_D = 'pin_d' -CONF_SLEEP_WHEN_DONE = 'sleep_when_done' -CONF_STEP_MODE = 'step_mode' -CONF_COMPONENTS = 'components' -CONF_DATA_TEMPLATE = 'data_template' -CONF_VARIABLES = 'variables' -CONF_SERVICE = 'service' -CONF_ENTITY_ID = 'entity_id' -CONF_RESTORE_MODE = 'restore_mode' -CONF_INTERVAL = 'interval' -CONF_DIRECTION = 'direction' -CONF_VARIANT = 'variant' -CONF_METHOD = 'method' -CONF_FAST_CONNECT = 'fast_connect' -CONF_INTERLOCK = 'interlock' -CONF_ON_TURN_ON = 'on_turn_on' -CONF_ON_TURN_OFF = 'on_turn_off' -CONF_USE_ADDRESS = 'use_address' -CONF_FROM = 'from' -CONF_TO = 'to' -CONF_SEGMENTS = 'segments' -CONF_MIN_POWER = 'min_power' -CONF_MIN_VALUE = 'min_value' -CONF_MAX_VALUE = 'max_value' -CONF_RX_ONLY = 'rx_only' - - ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_' ARDUINO_VERSION_ESP32_DEV = 'https://github.com/platformio/platform-espressif32.git#feature/stage' ARDUINO_VERSION_ESP32_1_0_0 = 'espressif32@1.5.0' @@ -461,3 +21,473 @@ ARDUINO_VERSION_ESP8266_DEV = 'https://github.com/platformio/platform-espressif8 '/stage' ARDUINO_VERSION_ESP8266_2_5_0 = 'espressif8266@2.0.0' ARDUINO_VERSION_ESP8266_2_3_0 = 'espressif8266@1.5.0' +SOURCE_FILE_EXTENSIONS = {'.cpp', '.hpp', '.h', '.c', '.tcc', '.ino'} +HEADER_FILE_EXTENSIONS = {'.h', '.hpp', '.tcc'} + +CONF_ABOVE = 'above' +CONF_ACCELERATION = 'acceleration' +CONF_ACCURACY = 'accuracy' +CONF_ACCURACY_DECIMALS = 'accuracy_decimals' +CONF_ACTION_ID = 'action_id' +CONF_ADDRESS = 'address' +CONF_ALPHA = 'alpha' +CONF_AND = 'and' +CONF_AP = 'ap' +CONF_ARDUINO_VERSION = 'arduino_version' +CONF_ARGS = 'args' +CONF_ASSUMED_STATE = 'assumed_state' +CONF_AT = 'at' +CONF_ATTENUATION = 'attenuation' +CONF_AUTOMATION_ID = 'automation_id' +CONF_AVAILABILITY = 'availability' +CONF_AWAY = 'away' +CONF_AWAY_CONFIG = 'away_config' +CONF_BATTERY_LEVEL = 'battery_level' +CONF_BAUD_RATE = 'baud_rate' +CONF_BELOW = 'below' +CONF_BINARY = 'binary' +CONF_BINARY_SENSOR = 'binary_sensor' +CONF_BINARY_SENSORS = 'binary_sensors' +CONF_BIRTH_MESSAGE = 'birth_message' +CONF_BIT_DEPTH = 'bit_depth' +CONF_BLUE = 'blue' +CONF_BOARD = 'board' +CONF_BOARD_FLASH_MODE = 'board_flash_mode' +CONF_BRANCH = 'branch' +CONF_BRIGHTNESS = 'brightness' +CONF_BROKER = 'broker' +CONF_BSSID = 'bssid' +CONF_BUFFER_SIZE = 'buffer_size' +CONF_BUILD_PATH = 'build_path' +CONF_BUSY_PIN = 'busy_pin' +CONF_BUS_VOLTAGE = 'bus_voltage' +CONF_CALIBRATE_LINEAR = 'calibrate_linear' +CONF_CARRIER_DUTY_PERCENT = 'carrier_duty_percent' +CONF_CARRIER_FREQUENCY = 'carrier_frequency' +CONF_CHANGE_MODE_EVERY = 'change_mode_every' +CONF_CHANNEL = 'channel' +CONF_CHANNELS = 'channels' +CONF_CHIPSET = 'chipset' +CONF_CLIENT_ID = 'client_id' +CONF_CLK_PIN = 'clk_pin' +CONF_CLOCK_PIN = 'clock_pin' +CONF_CLOSE_ACTION = 'close_action' +CONF_CLOSE_DURATION = 'close_duration' +CONF_CLOSE_ENDSTOP = 'close_endstop' +CONF_CO2 = 'co2' +CONF_CODE = 'code' +CONF_COLD_WHITE = 'cold_white' +CONF_COLD_WHITE_COLOR_TEMPERATURE = 'cold_white_color_temperature' +CONF_COLORS = 'colors' +CONF_COLOR_CORRECT = 'color_correct' +CONF_COLOR_TEMPERATURE = 'color_temperature' +CONF_COMMAND = 'command' +CONF_COMMAND_TOPIC = 'command_topic' +CONF_COMMIT = 'commit' +CONF_COMPONENTS = 'components' +CONF_COMPONENT_ID = 'component_id' +CONF_CONDITION = 'condition' +CONF_CONDITION_ID = 'condition_id' +CONF_CONDUCTIVITY = 'conductivity' +CONF_COOL_ACTION = 'cool_action' +CONF_COUNT_MODE = 'count_mode' +CONF_CRON = 'cron' +CONF_CSS_URL = 'css_url' +CONF_CS_PIN = 'cs_pin' +CONF_CURRENT = 'current' +CONF_CURRENT_OPERATION = 'current_operation' +CONF_CURRENT_RESISTOR = 'current_resistor' +CONF_DALLAS_ID = 'dallas_id' +CONF_DATA = 'data' +CONF_DATA_PIN = 'data_pin' +CONF_DATA_PINS = 'data_pins' +CONF_DATA_TEMPLATE = 'data_template' +CONF_DAYS_OF_MONTH = 'days_of_month' +CONF_DAYS_OF_WEEK = 'days_of_week' +CONF_DC_PIN = 'dc_pin' +CONF_DEBOUNCE = 'debounce' +CONF_DECELERATION = 'deceleration' +CONF_DEFAULT_TARGET_TEMPERATURE_HIGH = 'default_target_temperature_high' +CONF_DEFAULT_TARGET_TEMPERATURE_LOW = 'default_target_temperature_low' +CONF_DEFAULT_TRANSITION_LENGTH = 'default_transition_length' +CONF_DELAY = 'delay' +CONF_DELTA = 'delta' +CONF_DEVICE = 'device' +CONF_DEVICE_CLASS = 'device_class' +CONF_DIMENSIONS = 'dimensions' +CONF_DIRECTION = 'direction' +CONF_DIR_PIN = 'dir_pin' +CONF_DISCOVERY = 'discovery' +CONF_DISCOVERY_PREFIX = 'discovery_prefix' +CONF_DISCOVERY_RETAIN = 'discovery_retain' +CONF_DNS1 = 'dns1' +CONF_DNS2 = 'dns2' +CONF_DOMAIN = 'domain' +CONF_DUMP = 'dump' +CONF_DURATION = 'duration' +CONF_ECHO_PIN = 'echo_pin' +CONF_EFFECT = 'effect' +CONF_EFFECTS = 'effects' +CONF_ELSE = 'else' +CONF_ENABLE_PIN = 'enable_pin' +CONF_ENABLE_TIME = 'enable_time' +CONF_ENTITY_ID = 'entity_id' +CONF_ESP8266_RESTORE_FROM_FLASH = 'esp8266_restore_from_flash' +CONF_ESPHOME = 'esphome' +CONF_ESPHOME_CORE_VERSION = 'esphome_core_version' +CONF_EXPIRE_AFTER = 'expire_after' +CONF_EXTERNAL_VCC = 'external_vcc' +CONF_FALLING_EDGE = 'falling_edge' +CONF_FAMILY = 'family' +CONF_FAST_CONNECT = 'fast_connect' +CONF_FILE = 'file' +CONF_FILTER = 'filter' +CONF_FILTERS = 'filters' +CONF_FILTER_OUT = 'filter_out' +CONF_FLASH_LENGTH = 'flash_length' +CONF_FOR = 'for' +CONF_FORMALDEHYDE = 'formaldehyde' +CONF_FORMAT = 'format' +CONF_FREQUENCY = 'frequency' +CONF_FROM = 'from' +CONF_FULL_UPDATE_EVERY = 'full_update_every' +CONF_GAIN = 'gain' +CONF_GAMMA_CORRECT = 'gamma_correct' +CONF_GAS_RESISTANCE = 'gas_resistance' +CONF_GATEWAY = 'gateway' +CONF_GLYPHS = 'glyphs' +CONF_GPIO = 'gpio' +CONF_GREEN = 'green' +CONF_GROUP = 'group' +CONF_HARDWARE_UART = 'hardware_uart' +CONF_HEARTBEAT = 'heartbeat' +CONF_HEATER = 'heater' +CONF_HEAT_ACTION = 'heat_action' +CONF_HIDDEN = 'hidden' +CONF_HIGH = 'high' +CONF_HIGH_VOLTAGE_REFERENCE = 'high_voltage_reference' +CONF_HOUR = 'hour' +CONF_HOURS = 'hours' +CONF_HUMIDITY = 'humidity' +CONF_I2C = 'i2c' +CONF_I2C_ID = 'i2c_id' +CONF_ICON = 'icon' +CONF_ID = 'id' +CONF_IDLE = 'idle' +CONF_IDLE_ACTION = 'idle_action' +CONF_IDLE_LEVEL = 'idle_level' +CONF_IF = 'if' +CONF_IIR_FILTER = 'iir_filter' +CONF_ILLUMINANCE = 'illuminance' +CONF_INCLUDES = 'includes' +CONF_INDEX = 'index' +CONF_INITIAL_VALUE = 'initial_value' +CONF_INTEGRATION_TIME = 'integration_time' +CONF_INTENSITY = 'intensity' +CONF_INTERLOCK = 'interlock' +CONF_INTERNAL = 'internal' +CONF_INTERNAL_FILTER = 'internal_filter' +CONF_INTERVAL = 'interval' +CONF_INVALID_COOLDOWN = 'invalid_cooldown' +CONF_INVERT = 'invert' +CONF_INVERTED = 'inverted' +CONF_IP_ADDRESS = 'ip_address' +CONF_JS_URL = 'js_url' +CONF_JVC = 'jvc' +CONF_KEEPALIVE = 'keepalive' +CONF_KEEP_ON_TIME = 'keep_on_time' +CONF_LAMBDA = 'lambda' +CONF_LEVEL = 'level' +CONF_LG = 'lg' +CONF_LIBRARIES = 'libraries' +CONF_LIGHT = 'light' +CONF_LOCAL = 'local' +CONF_LOGGER = 'logger' +CONF_LOGS = 'logs' +CONF_LOG_TOPIC = 'log_topic' +CONF_LOW = 'low' +CONF_LOW_VOLTAGE_REFERENCE = 'low_voltage_reference' +CONF_MAC_ADDRESS = 'mac_address' +CONF_MAKE_ID = 'make_id' +CONF_MANUAL_IP = 'manual_ip' +CONF_MAX_CURRENT = 'max_current' +CONF_MAX_DURATION = 'max_duration' +CONF_MAX_LENGTH = 'max_length' +CONF_MAX_LEVEL = 'max_level' +CONF_MAX_POWER = 'max_power' +CONF_MAX_REFRESH_RATE = 'max_refresh_rate' +CONF_MAX_SPEED = 'max_speed' +CONF_MAX_TEMPERATURE = 'max_temperature' +CONF_MAX_VALUE = 'max_value' +CONF_MAX_VOLTAGE = 'max_voltage' +CONF_MEASUREMENT_DURATION = 'measurement_duration' +CONF_MEDIUM = 'medium' +CONF_METHOD = 'method' +CONF_MINUTE = 'minute' +CONF_MINUTES = 'minutes' +CONF_MIN_LENGTH = 'min_length' +CONF_MIN_LEVEL = 'min_level' +CONF_MIN_POWER = 'min_power' +CONF_MIN_TEMPERATURE = 'min_temperature' +CONF_MIN_VALUE = 'min_value' +CONF_MISO_PIN = 'miso_pin' +CONF_MODE = 'mode' +CONF_MODEL = 'model' +CONF_MOISTURE = 'moisture' +CONF_MONTHS = 'months' +CONF_MOSI_PIN = 'mosi_pin' +CONF_MQTT = 'mqtt' +CONF_MQTT_ID = 'mqtt_id' +CONF_MULTIPLEXER = 'multiplexer' +CONF_MULTIPLY = 'multiply' +CONF_NAME = 'name' +CONF_NBITS = 'nbits' +CONF_NEC = 'nec' +CONF_NETWORKS = 'networks' +CONF_NUMBER = 'number' +CONF_NUM_ATTEMPTS = 'num_attempts' +CONF_NUM_CHANNELS = 'num_channels' +CONF_NUM_CHIPS = 'num_chips' +CONF_NUM_LEDS = 'num_leds' +CONF_OFFSET = 'offset' +CONF_ON = 'on' +CONF_ONE = 'one' +CONF_ON_BOOT = 'on_boot' +CONF_ON_CLICK = 'on_click' +CONF_ON_DOUBLE_CLICK = 'on_double_click' +CONF_ON_JSON_MESSAGE = 'on_json_message' +CONF_ON_LOOP = 'on_loop' +CONF_ON_MESSAGE = 'on_message' +CONF_ON_MULTI_CLICK = 'on_multi_click' +CONF_ON_PRESS = 'on_press' +CONF_ON_RAW_VALUE = 'on_raw_value' +CONF_ON_RELEASE = 'on_release' +CONF_ON_SHUTDOWN = 'on_shutdown' +CONF_ON_STATE = 'on_state' +CONF_ON_TAG = 'on_tag' +CONF_ON_TIME = 'on_time' +CONF_ON_TURN_OFF = 'on_turn_off' +CONF_ON_TURN_ON = 'on_turn_on' +CONF_ON_VALUE = 'on_value' +CONF_ON_VALUE_RANGE = 'on_value_range' +CONF_OPEN_ACTION = 'open_action' +CONF_OPEN_DURATION = 'open_duration' +CONF_OPEN_ENDSTOP = 'open_endstop' +CONF_OPTIMISTIC = 'optimistic' +CONF_OR = 'or' +CONF_OSCILLATING = 'oscillating' +CONF_OSCILLATION_COMMAND_TOPIC = 'oscillation_command_topic' +CONF_OSCILLATION_OUTPUT = 'oscillation_output' +CONF_OSCILLATION_STATE_TOPIC = 'oscillation_state_topic' +CONF_OTA = 'ota' +CONF_OUTPUT = 'output' +CONF_OUTPUTS = 'outputs' +CONF_OUTPUT_ID = 'output_id' +CONF_OVERSAMPLING = 'oversampling' +CONF_PAGES = 'pages' +CONF_PAGE_ID = 'page_id' +CONF_PANASONIC = 'panasonic' +CONF_PASSWORD = 'password' +CONF_PAYLOAD = 'payload' +CONF_PAYLOAD_AVAILABLE = 'payload_available' +CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' +CONF_PHASE_BALANCER = 'phase_balancer' +CONF_PIN = 'pin' +CONF_PINS = 'pins' +CONF_PIN_A = 'pin_a' +CONF_PIN_B = 'pin_b' +CONF_PIN_C = 'pin_c' +CONF_PIN_D = 'pin_d' +CONF_PLATFORM = 'platform' +CONF_PLATFORMIO_OPTIONS = 'platformio_options' +CONF_PM_10_0 = 'pm_10_0' +CONF_PM_1_0 = 'pm_1_0' +CONF_PM_2_5 = 'pm_2_5' +CONF_PORT = 'port' +CONF_POSITION = 'position' +CONF_POWER = 'power' +CONF_POWER_ON_VALUE = 'power_on_value' +CONF_POWER_SAVE_MODE = 'power_save_mode' +CONF_POWER_SUPPLY = 'power_supply' +CONF_PRESSURE = 'pressure' +CONF_PRIORITY = 'priority' +CONF_PROTOCOL = 'protocol' +CONF_PULL_MODE = 'pull_mode' +CONF_PULSE_LENGTH = 'pulse_length' +CONF_QOS = 'qos' +CONF_RANDOM = 'random' +CONF_RANGE = 'range' +CONF_RATE = 'rate' +CONF_RAW = 'raw' +CONF_REBOOT_TIMEOUT = 'reboot_timeout' +CONF_RECEIVE_TIMEOUT = 'receive_timeout' +CONF_RED = 'red' +CONF_REPEAT = 'repeat' +CONF_REPOSITORY = 'repository' +CONF_RESET_PIN = 'reset_pin' +CONF_RESIZE = 'resize' +CONF_RESOLUTION = 'resolution' +CONF_RESTORE_MODE = 'restore_mode' +CONF_RESTORE_STATE = 'restore_state' +CONF_RESTORE_VALUE = 'restore_value' +CONF_RETAIN = 'retain' +CONF_RGBW = 'rgbw' +CONF_RGB_ORDER = 'rgb_order' +CONF_RISING_EDGE = 'rising_edge' +CONF_ROTATION = 'rotation' +CONF_RS_PIN = 'rs_pin' +CONF_RUN_CYCLES = 'run_cycles' +CONF_RUN_DURATION = 'run_duration' +CONF_RW_PIN = 'rw_pin' +CONF_RX_ONLY = 'rx_only' +CONF_RX_PIN = 'rx_pin' +CONF_SAFE_MODE = 'safe_mode' +CONF_SAMSUNG = 'samsung' +CONF_SCAN = 'scan' +CONF_SCAN_INTERVAL = 'scan_interval' +CONF_SCL = 'scl' +CONF_SDA = 'sda' +CONF_SECOND = 'second' +CONF_SECONDS = 'seconds' +CONF_SEGMENTS = 'segments' +CONF_SEL_PIN = 'sel_pin' +CONF_SEND_EVERY = 'send_every' +CONF_SEND_FIRST_AT = 'send_first_at' +CONF_SENSOR = 'sensor' +CONF_SENSORS = 'sensors' +CONF_SENSOR_ID = 'sensor_id' +CONF_SERVERS = 'servers' +CONF_SERVICE = 'service' +CONF_SERVICES = 'services' +CONF_SETUP_MODE = 'setup_mode' +CONF_SETUP_PRIORITY = 'setup_priority' +CONF_SHUNT_RESISTANCE = 'shunt_resistance' +CONF_SHUNT_VOLTAGE = 'shunt_voltage' +CONF_SHUTDOWN_MESSAGE = 'shutdown_message' +CONF_SIZE = 'size' +CONF_SLEEP_DURATION = 'sleep_duration' +CONF_SLEEP_PIN = 'sleep_pin' +CONF_SLEEP_WHEN_DONE = 'sleep_when_done' +CONF_SONY = 'sony' +CONF_SPEED = 'speed' +CONF_SPEED_COMMAND_TOPIC = 'speed_command_topic' +CONF_SPEED_STATE_TOPIC = 'speed_state_topic' +CONF_SPI_ID = 'spi_id' +CONF_SSID = 'ssid' +CONF_SSL_FINGERPRINTS = 'ssl_fingerprints' +CONF_STATE = 'state' +CONF_STATE_TOPIC = 'state_topic' +CONF_STATIC_IP = 'static_ip' +CONF_STEP_MODE = 'step_mode' +CONF_STEP_PIN = 'step_pin' +CONF_STOP = 'stop' +CONF_STOP_ACTION = 'stop_action' +CONF_SUBNET = 'subnet' +CONF_SWITCHES = 'switches' +CONF_SYNC = 'sync' +CONF_TAG = 'tag' +CONF_TARGET = 'target' +CONF_TARGET_TEMPERATURE = 'target_temperature' +CONF_TARGET_TEMPERATURE_HIGH = 'target_temperature_high' +CONF_TARGET_TEMPERATURE_LOW = 'target_temperature_low' +CONF_TEMPERATURE = 'temperature' +CONF_TEMPERATURE_STEP = 'temperature_step' +CONF_TEXT_SENSORS = 'text_sensors' +CONF_THEN = 'then' +CONF_THRESHOLD = 'threshold' +CONF_THROTTLE = 'throttle' +CONF_TILT = 'tilt' +CONF_TIMEOUT = 'timeout' +CONF_TIMES = 'times' +CONF_TIMEZONE = 'timezone' +CONF_TIME_ID = 'time_id' +CONF_TIMING = 'timing' +CONF_TO = 'to' +CONF_TOLERANCE = 'tolerance' +CONF_TOPIC = 'topic' +CONF_TOPIC_PREFIX = 'topic_prefix' +CONF_TRANSITION_LENGTH = 'transition_length' +CONF_TRIGGER_ID = 'trigger_id' +CONF_TRIGGER_PIN = 'trigger_pin' +CONF_TURN_OFF_ACTION = 'turn_off_action' +CONF_TURN_ON_ACTION = 'turn_on_action' +CONF_TX_BUFFER_SIZE = 'tx_buffer_size' +CONF_TX_PIN = 'tx_pin' +CONF_TYPE = 'type' +CONF_UART_ID = 'uart_id' +CONF_UID = 'uid' +CONF_UNIQUE = 'unique' +CONF_UNIT_OF_MEASUREMENT = 'unit_of_measurement' +CONF_UPDATE_INTERVAL = 'update_interval' +CONF_UPDATE_ON_BOOT = 'update_on_boot' +CONF_USERNAME = 'username' +CONF_USE_ADDRESS = 'use_address' +CONF_UUID = 'uuid' +CONF_VALUE = 'value' +CONF_VARIABLES = 'variables' +CONF_VARIANT = 'variant' +CONF_VISUAL = 'visual' +CONF_VOLTAGE = 'voltage' +CONF_VOLTAGE_ATTENUATION = 'voltage_attenuation' +CONF_VOLTAGE_DIVIDER = 'voltage_divider' +CONF_WAIT_TIME = 'wait_time' +CONF_WAIT_UNTIL = 'wait_until' +CONF_WAKEUP_PIN = 'wakeup_pin' +CONF_WARM_WHITE = 'warm_white' +CONF_WARM_WHITE_COLOR_TEMPERATURE = 'warm_white_color_temperature' +CONF_WHILE = 'while' +CONF_WHITE = 'white' +CONF_WIDTH = 'width' +CONF_WIFI = 'wifi' +CONF_WILL_MESSAGE = 'will_message' +CONF_WINDOW_SIZE = 'window_size' +CONF_ZERO = 'zero' + +ICON_ARROW_EXPAND_VERTICAL = 'mdi:arrow-expand-vertical' +ICON_BATTERY = 'mdi:battery' +ICON_BRIEFCASE_DOWNLOAD = 'mdi:briefcase-download' +ICON_BRIGHTNESS_5 = 'mdi:brightness-5' +ICON_CHEMICAL_WEAPON = 'mdi:chemical-weapon' +ICON_FLASH = 'mdi:flash' +ICON_FLOWER = 'mdi:flower' +ICON_GAS_CYLINDER = 'mdi:gas-cylinder' +ICON_GAUGE = 'mdi:gauge' +ICON_LIGHTBULB = 'mdi:lightbulb' +ICON_MAGNET = 'mdi:magnet' +ICON_NEW_BOX = 'mdi:new-box' +ICON_PERCENT = 'mdi:percent' +ICON_PERIODIC_TABLE_CO2 = 'mdi:periodic-table-co2' +ICON_POWER = 'mdi:power' +ICON_PULSE = 'mdi:pulse' +ICON_RESTART = 'mdi:restart' +ICON_SCALE = 'mdi:scale' +ICON_SCREEN_ROTATION = 'mdi:screen-rotation' +ICON_SIGNAL = 'mdi:signal' +ICON_THERMOMETER = 'mdi:thermometer' +ICON_TIMER = 'mdi:timer' +ICON_WATER_PERCENT = 'mdi:water-percent' +ICON_WIFI = 'mdi:wifi' + +UNIT_AMPERE = 'A' +UNIT_CELSIUS = u'°C' +UNIT_DECIBEL = 'dB' +UNIT_DEGREES = u'°' +UNIT_DEGREE_PER_SECOND = u'°/s' +UNIT_HECTOPASCAL = 'hPa' +UNIT_KELVIN = 'K' +UNIT_LUX = 'lx' +UNIT_METER = 'm' +UNIT_METER_PER_SECOND_SQUARED = u'm/s²' +UNIT_MICROGRAMS_PER_CUBIC_METER = u'µg/m³' +UNIT_MICROSIEMENS_PER_CENTIMETER = u'µS/cm' +UNIT_MICROTESLA = u'µT' +UNIT_OHM = u'Ω' +UNIT_PARTS_PER_MILLION = 'ppm' +UNIT_PERCENT = '%' +UNIT_PULSES_PER_MINUTE = 'pulses/min' +UNIT_SECOND = 's' +UNIT_VOLT = 'V' +UNIT_WATT = 'W' + +DEVICE_CLASS_CONNECTIVITY = 'connectivity' +DEVICE_CLASS_MOVING = 'moving' diff --git a/esphome/core.py b/esphome/core.py index 657228641..1f2794aff 100644 --- a/esphome/core.py +++ b/esphome/core.py @@ -1,6 +1,5 @@ -import collections -from collections import OrderedDict import functools +import heapq import inspect import logging import math @@ -10,11 +9,11 @@ import re # pylint: disable=unused-import, wrong-import-order from typing import Any, Dict, List # noqa -from esphome.const import CONF_ARDUINO_VERSION, CONF_ESPHOME, CONF_ESPHOME_CORE_VERSION, \ - CONF_LOCAL, CONF_USE_ADDRESS, CONF_WIFI, ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266, \ - CONF_REPOSITORY, CONF_BRANCH +from esphome.const import CONF_ARDUINO_VERSION, CONF_ESPHOME, CONF_USE_ADDRESS, CONF_WIFI, \ + SOURCE_FILE_EXTENSIONS from esphome.helpers import ensure_unique_string, is_hassio -from esphome.py_compat import IS_PY2, integer_types +from esphome.py_compat import IS_PY2, integer_types, text_type, string_types +from esphome.util import OrderedDict _LOGGER = logging.getLogger(__name__) @@ -55,6 +54,7 @@ class MACAddress(object): def __str__(self): return ':'.join('{:02X}'.format(part) for part in self.parts) + @property def as_hex(self): from esphome.cpp_generator import RawExpression @@ -286,37 +286,115 @@ class ID(object): def __eq__(self, other): if not isinstance(other, ID): - raise ValueError("other must be ID") + raise ValueError("other must be ID {} {}".format(type(other), other)) return self.id == other.id def __hash__(self): return hash(self.id) -def coroutine(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - if not inspect.isgeneratorfunction(func): - yield func(*args, **kwargs) - return - gen = func(*args, **kwargs) - var = None - try: - while True: - var = gen.send(var) - if inspect.isgenerator(var): - # Yielded generator, equivalent to 'yield from' - for x in var: - yield None - # Last yield value is the result - var = x - else: - yield var - except StopIteration: - # Stopping iteration - yield var +class Define(object): + def __init__(self, name, value=None): + self.name = name + self.value = value - return wrapper + @property + def as_build_flag(self): + if self.value is None: + return u'-D{}'.format(self.name) + return u'-D{}={}'.format(self.name, self.value) + + @property + def as_macro(self): + if self.value is None: + return u'#define {}'.format(self.name) + return u'#define {} {}'.format(self.name, self.value) + + @property + def as_tuple(self): + return self.name, self.value + + def __hash__(self): + return hash(self.as_tuple) + + def __eq__(self, other): + return isinstance(self, type(other)) and self.as_tuple == other.as_tuple + + +class Library(object): + def __init__(self, name, version): + self.name = name + self.version = version + + @property + def as_lib_dep(self): + if self.version is None: + return self.name + return u'{}@{}'.format(self.name, self.version) + + @property + def as_tuple(self): + return self.name, self.version + + def __hash__(self): + return hash(self.as_tuple) + + def __eq__(self, other): + return isinstance(self, type(other)) and self.as_tuple == other.as_tuple + + +def coroutine(func): + return coroutine_with_priority(0.0)(func) + + +def coroutine_with_priority(priority): + def decorator(func): + if getattr(func, '_esphome_coroutine', False): + # If func is already a coroutine, do not re-wrap it (performance) + return func + + @functools.wraps(func) + def wrapper(*args, **kwargs): + if not inspect.isgeneratorfunction(func): + # If func is not a generator, return result immediately + yield func(*args, **kwargs) + return + gen = func(*args, **kwargs) + var = None + try: + while True: + var = gen.send(var) + if inspect.isgenerator(var): + # Yielded generator, equivalent to 'yield from' + x = None + for x in var: + yield None + # Last yield value is the result + var = x + else: + yield var + except StopIteration: + # Stopping iteration + yield var + + # pylint: disable=protected-access + wrapper._esphome_coroutine = True + wrapper.priority = priority + return wrapper + return decorator + + +def find_source_files(file): + files = set() + directory = os.path.abspath(os.path.dirname(file)) + for f in os.listdir(directory): + if not os.path.isfile(os.path.join(directory, f)): + continue + _, ext = os.path.splitext(f) + if ext.lower() not in SOURCE_FILE_EXTENSIONS: + continue + files.add(f) + return files # pylint: disable=too-many-instance-attributes,too-many-public-methods @@ -339,11 +417,41 @@ class EsphomeCore(object): # The validated configuration, this is None until the config has been validated self.config = {} # type: ConfigType # The pending tasks in the task queue (mostly for C++ generation) - self.pending_tasks = collections.deque() + # This is a priority queue (with heapq) + # Each item is a tuple of form: (-priority, unique number, task) + self.pending_tasks = [] + # Task counter for pending tasks + self.task_counter = 0 # The variable cache, for each ID this holds a MockObj of the variable obj self.variables = {} # type: Dict[str, MockObj] - # The list of expressions for the C++ generation - self.expressions = [] # type: List[Expression] + # A list of statements that go in the main setup() block + self.main_statements = [] # type: List[Statement] + # A list of statements to insert in the global block (includes and global variables) + self.global_statements = [] # type: List[Statement] + # A set of platformio libraries to add to the project + self.libraries = set() # type: Set[Library] + # A set of build flags to set in the platformio project + self.build_flags = set() # type: Set[str] + # A set of defines to set for the compile process in esphome/core/defines.h + self.defines = set() # type: Set[Define] + + def reset(self): + self.dashboard = False + self.name = None + self.config_path = None + self.build_path = None + self.esp_platform = None + self.board = None + self.raw_config = None + self.config = None + self.pending_tasks = [] + self.task_counter = 0 + self.variables = {} + self.main_statements = [] + self.global_statements = [] + self.libraries = set() + self.build_flags = set() + self.defines = set() @property def address(self): # type: () -> str @@ -355,20 +463,6 @@ class EsphomeCore(object): return None - @property - def esphome_core_version(self): # type: () -> Dict[str, str] - return self.config[CONF_ESPHOME][CONF_ESPHOME_CORE_VERSION] - - @property - def is_dev_esphome_core_version(self): - if CONF_REPOSITORY not in self.esphome_core_version: - return False - return self.esphome_core_version.get(CONF_BRANCH) == 'dev' - - @property - def is_local_esphome_core_copy(self): - return CONF_LOCAL in self.esphome_core_version - @property def arduino_version(self): # type: () -> str return self.config[CONF_ESPHOME][CONF_ARDUINO_VERSION] @@ -389,6 +483,9 @@ class EsphomeCore(object): path_ = os.path.expanduser(os.path.join(*path)) return os.path.join(self.build_path, path_) + def relative_src_path(self, *path): + return self.relative_build_path('src', *path) + def relative_pioenvs_path(self, *path): if is_hassio(): return os.path.join('/data', self.name, '.pioenvs', *path) @@ -407,19 +504,21 @@ class EsphomeCore(object): def is_esp8266(self): if self.esp_platform is None: raise ValueError - return self.esp_platform == ESP_PLATFORM_ESP8266 + return self.esp_platform == 'ESP8266' @property def is_esp32(self): if self.esp_platform is None: raise ValueError - return self.esp_platform == ESP_PLATFORM_ESP32 + return self.esp_platform == 'ESP32' def add_job(self, func, *args, **kwargs): - domain = kwargs.get('domain') - gen = coroutine(func)(*args) - self.pending_tasks.append((gen, domain)) - return gen + coro = coroutine(func) + task = coro(*args, **kwargs) + item = (-coro.priority, self.task_counter, task) + self.task_counter += 1 + heapq.heappush(self.pending_tasks, item) + return task def flush_tasks(self): i = 0 @@ -428,29 +527,78 @@ class EsphomeCore(object): if i > 1000000: raise EsphomeError("Circular dependency detected!") - task, domain = self.pending_tasks.popleft() - _LOGGER.debug("Executing task for domain=%s", domain) + inv_priority, num, task = heapq.heappop(self.pending_tasks) + priority = -inv_priority + _LOGGER.debug("Running %s (num %s)", task, num) try: next(task) - self.pending_tasks.append((task, domain)) + # Decrease priority over time, so that if this task is blocked + # due to a dependency others will clear the dependency + # This could be improved with a less naive approach + priority -= 1 + item = (-priority, num, task) + heapq.heappush(self.pending_tasks, item) except StopIteration: - _LOGGER.debug(" -> %s finished", domain) + _LOGGER.debug(" -> finished") - def add(self, expression, require=True): - from esphome.cpp_generator import Expression + def add(self, expression): + from esphome.cpp_generator import Expression, Statement, statement - if require and isinstance(expression, Expression): - expression.require() - self.expressions.append(expression) + if isinstance(expression, Expression): + expression = statement(expression) + if not isinstance(expression, Statement): + raise ValueError(u"Add '{}' must be expression or statement, not {}" + u"".format(expression, type(expression))) + + self.main_statements.append(expression) _LOGGER.debug("Adding: %s", expression) return expression + def add_global(self, expression): + from esphome.cpp_generator import Expression, Statement, statement + + if isinstance(expression, Expression): + expression = statement(expression) + if not isinstance(expression, Statement): + raise ValueError(u"Add '{}' must be expression or statement, not {}" + u"".format(expression, type(expression))) + self.global_statements.append(expression) + _LOGGER.debug("Adding global: %s", expression) + return expression + + def add_library(self, library): + if not isinstance(library, Library): + raise ValueError(u"Library {} must be instance of Library, not {}" + u"".format(library, type(library))) + self.libraries.add(library) + _LOGGER.debug("Adding library: %s", library) + return library + + def add_build_flag(self, build_flag): + self.build_flags.add(build_flag) + _LOGGER.debug("Adding build flag: %s", build_flag) + return build_flag + + def add_define(self, define): + if isinstance(define, string_types): + define = Define(define) + elif isinstance(define, Define): + pass + else: + raise ValueError(u"Define {} must be string or Define, not {}" + u"".format(define, type(define))) + self.defines.add(define) + _LOGGER.debug("Adding define: %s", define) + return define + def get_variable(self, id): + if not isinstance(id, ID): + raise ValueError("ID {!r} must be of type ID!".format(id)) while True: if id in self.variables: yield self.variables[id] return - _LOGGER.debug("Waiting for variable %s", id) + _LOGGER.debug("Waiting for variable %s (%r)", id, id) yield None def get_variable_with_full_id(self, id): @@ -472,6 +620,32 @@ class EsphomeCore(object): def has_id(self, id): return id in self.variables + @property + def cpp_main_section(self): + from esphome.cpp_generator import statement + + main_code = [] + for exp in self.main_statements: + text = text_type(statement(exp)) + text = text.rstrip() + main_code.append(text) + return u'\n'.join(main_code) + u'\n\n' + + @property + def cpp_global_section(self): + from esphome.cpp_generator import statement + + global_code = [] + for exp in self.global_statements: + text = text_type(statement(exp)) + text = text.rstrip() + global_code.append(text) + return u'\n'.join(global_code) + u'\n' + + +class AutoLoad(dict): + pass + CORE = EsphomeCore() diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp new file mode 100644 index 000000000..99d5f7fe2 --- /dev/null +++ b/esphome/core/application.cpp @@ -0,0 +1,113 @@ +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +#ifdef USE_STATUS_LED +#include "esphome/components/status_led/status_led.h" +#endif + +namespace esphome { + +static const char *TAG = "app"; + +void Application::register_component_(Component *comp) { + if (comp == nullptr) { + ESP_LOGW(TAG, "Tried to register null component!"); + return; + } + + for (auto *c : this->components_) { + if (comp == c) { + ESP_LOGW(TAG, "Component already registered! (%p)", c); + return; + } + } + this->components_.push_back(comp); +} +void Application::setup() { + ESP_LOGI(TAG, "Running through setup()..."); + ESP_LOGV(TAG, "Sorting components by setup priority..."); + std::stable_sort(this->components_.begin(), this->components_.end(), [](const Component *a, const Component *b) { + return a->get_actual_setup_priority() > b->get_actual_setup_priority(); + }); + + for (uint32_t i = 0; i < this->components_.size(); i++) { + Component *component = this->components_[i]; + if (component->is_failed()) + continue; + + component->call_setup(); + if (component->can_proceed()) + continue; + + std::stable_sort(this->components_.begin(), this->components_.begin() + i + 1, + [](Component *a, Component *b) { return a->get_loop_priority() > b->get_loop_priority(); }); + + do { + uint32_t new_app_state = STATUS_LED_WARNING; + for (uint32_t j = 0; j <= i; j++) { + if (!this->components_[j]->is_failed()) { + this->components_[j]->call_loop(); + } + new_app_state |= this->components_[j]->get_component_state(); + this->app_state_ |= new_app_state; + } + this->app_state_ = new_app_state; + yield(); + } while (!component->can_proceed()); + } + + ESP_LOGI(TAG, "setup() finished successfully!"); + this->dump_config(); +} +void Application::dump_config() { + ESP_LOGI(TAG, "esphome-core version " ESPHOME_VERSION " compiled on %s", this->compilation_time_.c_str()); + + for (auto component : this->components_) { + component->dump_config(); + } +} + +void ICACHE_RAM_ATTR HOT Application::feed_wdt() { + static uint32_t LAST_FEED = 0; + uint32_t now = millis(); + if (now - LAST_FEED > 3) { +#ifdef ARDUINO_ARCH_ESP8266 + ESP.wdtFeed(); +#endif +#ifdef ARDUINO_ARCH_ESP32 + yield(); +#endif + LAST_FEED = now; +#ifdef USE_STATUS_LED + if (status_led::global_status_led != nullptr) { + status_led::global_status_led->call_loop(); + } +#endif + } +} +void Application::reboot() { + ESP_LOGI(TAG, "Forcing a reboot..."); + for (auto *comp : this->components_) + comp->on_shutdown(); + ESP.restart(); + // restart() doesn't always end execution + while (true) { + yield(); + } +} +void Application::safe_reboot() { + ESP_LOGI(TAG, "Rebooting safely..."); + for (auto *comp : this->components_) + comp->on_safe_shutdown(); + for (auto *comp : this->components_) + comp->on_shutdown(); + ESP.restart(); + // restart() doesn't always end execution + while (true) { + yield(); + } +} + +Application App; + +} // namespace esphome diff --git a/esphome/core/application.h b/esphome/core/application.h new file mode 100644 index 000000000..8c36da6ae --- /dev/null +++ b/esphome/core/application.h @@ -0,0 +1,271 @@ +#pragma once + +#include +#include +#include "esphome/core/defines.h" +#include "esphome/core/preferences.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.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_SWITCH +#include "esphome/components/switch/switch.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif +#ifdef USE_FAN +#include "esphome/components/fan/fan_state.h" +#endif +#ifdef USE_CLIMATE +#include "esphome/components/climate/climate.h" +#endif +#ifdef USE_LIGHT +#include "esphome/components/light/light_state.h" +#endif +#ifdef USE_COVER +#include "esphome/components/cover/cover.h" +#endif + +namespace esphome { + +class Application { + public: + void pre_setup(const std::string &name, const char *compilation_time) { + this->name_ = name; + this->compilation_time_ = compilation_time; + global_preferences.begin(this->name_); + } + +#ifdef USE_BINARY_SENSOR + void register_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { + this->binary_sensors_.push_back(binary_sensor); + } +#endif + +#ifdef USE_SENSOR + void register_sensor(sensor::Sensor *sensor) { this->sensors_.push_back(sensor); } +#endif + +#ifdef USE_SWITCH + void register_switch(switch_::Switch *a_switch) { this->switches_.push_back(a_switch); } +#endif + +#ifdef USE_TEXT_SENSOR + void register_text_sensor(text_sensor::TextSensor *sensor) { this->text_sensors_.push_back(sensor); } +#endif + +#ifdef USE_FAN + void register_fan(fan::FanState *state) { this->fans_.push_back(state); } +#endif + +#ifdef USE_COVER + void register_cover(cover::Cover *cover) { this->covers_.push_back(cover); } +#endif + +#ifdef USE_CLIMATE + void register_climate(climate::Climate *climate) { this->climates_.push_back(climate); } +#endif + +#ifdef USE_LIGHT + void register_light(light::LightState *light) { this->lights_.push_back(light); } +#endif + + /// Register the component in this Application instance. + template C *register_component(C *c) { + static_assert(std::is_base_of::value, "Only Component subclasses can be registered"); + this->register_component_((Component *) c); + return c; + } + + /// Set up all the registered components. Call this at the end of your setup() function. + void setup(); + + /// Make a loop iteration. Call this in your loop() function. + void loop() { + uint32_t new_app_state = 0; + for (Component *component : this->components_) { + if (!component->is_failed()) { + component->call_loop(); + } + new_app_state |= component->get_component_state(); + this->app_state_ |= new_app_state; + this->feed_wdt(); + } + this->app_state_ = new_app_state; + + const uint32_t now = millis(); + if (HighFrequencyLoopRequester::is_high_frequency()) { + yield(); + } else { + uint32_t delay_time = this->loop_interval_; + if (now - this->last_loop_ < this->loop_interval_) + delay_time = this->loop_interval_ - (now - this->last_loop_); + delay(delay_time); + } + this->last_loop_ = now; + + if (this->dump_config_scheduled_) { + this->dump_config(); + this->dump_config_scheduled_ = false; + } + } + + /// Get the name of this Application set by set_name(). + const std::string &get_name() const { return this->name_; } + + const std::string &get_compilation_time() const { return this->compilation_time_; } + + /** Set the target interval with which to run the loop() calls. + * If the loop() method takes longer than the target interval, ESPHome won't + * sleep in loop(), but if the time spent in loop() is small than the target, ESPHome + * will delay at the end of the App.loop() method. + * + * This is done to conserve power: In most use-cases, high-speed loop() calls are not required + * and degrade power consumption. + * + * Each component can request a high frequency loop execution by using the HighFrequencyLoopRequester + * helper in helpers.h + * + * @param loop_interval The interval in milliseconds to run the core loop at. Defaults to 16 milliseconds. + */ + void set_loop_interval(uint32_t loop_interval) { this->loop_interval_ = loop_interval; } + + void dump_config(); + void schedule_dump_config() { this->dump_config_scheduled_ = true; } + + void feed_wdt(); + + void reboot(); + + void safe_reboot(); + + void run_safe_shutdown_hooks() { + for (auto *comp : this->components_) + comp->on_safe_shutdown(); + } + + uint32_t get_app_state() const { return this->app_state_; } + +#ifdef USE_BINARY_SENSOR + const std::vector &get_binary_sensors() { return this->binary_sensors_; } + binary_sensor::BinarySensor *get_binary_sensor_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->binary_sensors_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif +#ifdef USE_SWITCH + const std::vector &get_switches() { return this->switches_; } + switch_::Switch *get_switch_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->switches_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif +#ifdef USE_SENSOR + const std::vector &get_sensors() { return this->sensors_; } + sensor::Sensor *get_sensor_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->sensors_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif +#ifdef USE_TEXT_SENSOR + const std::vector &get_text_sensors() { return this->text_sensors_; } + text_sensor::TextSensor *get_text_sensor_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->text_sensors_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif +#ifdef USE_FAN + const std::vector &get_fans() { return this->fans_; } + fan::FanState *get_fan_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->fans_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif +#ifdef USE_COVER + const std::vector &get_covers() { return this->covers_; } + cover::Cover *get_cover_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->covers_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif +#ifdef USE_LIGHT + const std::vector &get_lights() { return this->lights_; } + light::LightState *get_light_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->lights_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif +#ifdef USE_CLIMATE + const std::vector &get_climates() { return this->climates_; } + climate::Climate *get_climate_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->climates_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif + + protected: + friend Component; + + void register_component_(Component *comp); + + std::vector components_{}; + +#ifdef USE_BINARY_SENSOR + std::vector binary_sensors_{}; +#endif +#ifdef USE_SWITCH + std::vector switches_{}; +#endif +#ifdef USE_SENSOR + std::vector sensors_{}; +#endif +#ifdef USE_TEXT_SENSOR + std::vector text_sensors_{}; +#endif +#ifdef USE_FAN + std::vector fans_{}; +#endif +#ifdef USE_COVER + std::vector covers_{}; +#endif +#ifdef USE_CLIMATE + std::vector climates_{}; +#endif +#ifdef USE_LIGHT + std::vector lights_{}; +#endif + + std::string name_; + std::string compilation_time_; + uint32_t last_loop_{0}; + uint32_t loop_interval_{16}; + bool dump_config_scheduled_{false}; + uint32_t app_state_{0}; +}; + +/// Global storage of Application pointer - only one Application can exist. +extern Application App; + +} // namespace esphome diff --git a/esphome/core/automation.cpp b/esphome/core/automation.cpp new file mode 100644 index 000000000..4df8fc974 --- /dev/null +++ b/esphome/core/automation.cpp @@ -0,0 +1,33 @@ +#include "esphome/core/automation.h" + +namespace esphome { + +static const char *TAG = "automation"; + +void StartupTrigger::setup() { this->trigger(); } +float StartupTrigger::get_setup_priority() const { + // Run after everything is set up + return this->setup_priority_; +} +StartupTrigger::StartupTrigger(float setup_priority) : setup_priority_(setup_priority) {} + +void ShutdownTrigger::on_shutdown() { this->trigger(); } + +void LoopTrigger::loop() { this->trigger(); } +float LoopTrigger::get_setup_priority() const { return setup_priority::DATA; } + +RangeCondition::RangeCondition() = default; + +bool RangeCondition::check(float x) { + float min = this->min_.value(x); + float max = this->max_.value(x); + if (isnan(min)) { + return x >= max; + } else if (isnan(max)) { + return x >= min; + } else { + return min <= x && x <= max; + } +} + +} // namespace esphome diff --git a/esphome/core/automation.h b/esphome/core/automation.h new file mode 100644 index 000000000..5fac5b888 --- /dev/null +++ b/esphome/core/automation.h @@ -0,0 +1,241 @@ +#pragma once + +#include +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/defines.h" +#include "esphome/core/preferences.h" + +namespace esphome { + +#define TEMPLATABLE_VALUE_(type, name) \ + protected: \ + TemplatableValue name##_{}; \ +\ + public: \ + template void set_##name(V value_) { this->name##_ = value_; } + +#define TEMPLATABLE_VALUE(type, name) TEMPLATABLE_VALUE_(type, name) + +template class Condition { + public: + virtual bool check(Ts... x) = 0; + + bool check_tuple(const std::tuple &tuple); + + protected: + template bool check_tuple_(const std::tuple &tuple, seq); +}; + +template class AndCondition : public Condition { + public: + explicit AndCondition(const std::vector *> &conditions); + bool check(Ts... x) override; + + protected: + std::vector *> conditions_; +}; + +template class OrCondition : public Condition { + public: + explicit OrCondition(const std::vector *> &conditions); + bool check(Ts... x) override; + + protected: + std::vector *> conditions_; +}; + +template class LambdaCondition : public Condition { + public: + explicit LambdaCondition(std::function &&f); + bool check(Ts... x) override; + + protected: + std::function f_; +}; + +class RangeCondition : public Condition { + public: + explicit RangeCondition(); + bool check(float x) override; + + template void set_min(V value) { this->min_ = value; } + template void set_max(V value) { this->max_ = value; } + + protected: + TemplatableValue min_{NAN}; + TemplatableValue max_{NAN}; +}; + +template class Automation; + +template class Trigger { + public: + void trigger(Ts... x); + void set_parent(Automation *parent); + void stop(); + + protected: + Automation *parent_{nullptr}; +}; + +class StartupTrigger : public Trigger<>, public Component { + public: + explicit StartupTrigger(float setup_priority = setup_priority::LATE); + void setup() override; + float get_setup_priority() const override; + + protected: + float setup_priority_; +}; + +class ShutdownTrigger : public Trigger<>, public Component { + public: + void on_shutdown() override; +}; + +class LoopTrigger : public Trigger<>, public Component { + public: + void loop() override; + float get_setup_priority() const override; +}; + +template class ActionList; + +template class Action { + public: + virtual void play(Ts... x) = 0; + void play_next(Ts... x); + virtual void stop(); + void stop_next(); + + void play_next_tuple(const std::tuple &tuple); + + protected: + friend ActionList; + + template void play_next_tuple_(const std::tuple &tuple, seq); + + Action *next_ = nullptr; +}; + +template class DelayAction : public Action, public Component { + public: + explicit DelayAction(); + + template void set_delay(V value) { this->delay_ = value; } + void stop() override; + + void play(Ts... x) override; + float get_setup_priority() const override; + + protected: + TemplatableValue delay_{0}; +}; + +template class LambdaAction : public Action { + public: + explicit LambdaAction(std::function &&f); + void play(Ts... x) override; + + protected: + std::function f_; +}; + +template class IfAction : public Action { + public: + explicit IfAction(std::vector *> conditions); + + void add_then(const std::vector *> &actions); + + void add_else(const std::vector *> &actions); + + void play(Ts... x) override; + + void stop() override; + + protected: + std::vector *> conditions_; + ActionList then_; + ActionList else_; +}; + +template class WhileAction : public Action { + public: + WhileAction(const std::vector *> &conditions); + + void add_then(const std::vector *> &actions); + + void play(Ts... x) override; + + void stop() override; + + protected: + std::vector *> conditions_; + ActionList then_; + bool is_running_{false}; +}; + +template class WaitUntilAction : public Action, public Component { + public: + WaitUntilAction(const std::vector *> &conditions); + + void play(Ts... x) override; + + void stop() override; + + void loop() override; + + float get_setup_priority() const override; + + protected: + std::vector *> conditions_; + bool triggered_{false}; + std::tuple var_{}; +}; + +template class UpdateComponentAction : public Action { + public: + UpdateComponentAction(PollingComponent *component); + void play(Ts... x) override; + + protected: + PollingComponent *component_; +}; + +template class ActionList { + public: + Action *add_action(Action *action); + void add_actions(const std::vector *> &actions); + void play(Ts... x); + void stop(); + bool empty() const; + + protected: + Action *actions_begin_{nullptr}; + Action *actions_end_{nullptr}; +}; + +template class Automation { + public: + explicit Automation(Trigger *trigger); + + Condition *add_condition(Condition *condition); + void add_conditions(const std::vector *> &conditions); + + Action *add_action(Action *action); + void add_actions(const std::vector *> &actions); + + void stop(); + + void trigger(Ts... x); + + protected: + Trigger *trigger_; + std::vector *> conditions_; + ActionList actions_; +}; + +} // namespace esphome + +#include "esphome/core/automation.tcc" diff --git a/esphome/core/automation.tcc b/esphome/core/automation.tcc new file mode 100644 index 000000000..a8ecae7a4 --- /dev/null +++ b/esphome/core/automation.tcc @@ -0,0 +1,243 @@ +#pragma once +#include "esphome/core/automation.h" + +namespace esphome { + +template bool Condition::check_tuple(const std::tuple &tuple) { + return this->check_tuple_(tuple, typename gens::type()); +} +template +template +bool Condition::check_tuple_(const std::tuple &tuple, seq) { + return this->check(std::get(tuple)...); +} + +template bool AndCondition::check(Ts... x) { + for (auto *condition : this->conditions_) { + if (!condition->check(x...)) + return false; + } + + return true; +} + +template +AndCondition::AndCondition(const std::vector *> &conditions) : conditions_(conditions) {} + +template bool OrCondition::check(Ts... x) { + for (auto *condition : this->conditions_) { + if (condition->check(x...)) + return true; + } + + return false; +} + +template +OrCondition::OrCondition(const std::vector *> &conditions) : conditions_(conditions) {} + +template void Trigger::set_parent(Automation *parent) { this->parent_ = parent; } + +template void Trigger::trigger(Ts... x) { + if (this->parent_ == nullptr) + return; + this->parent_->trigger(x...); +} +template void Trigger::stop() { + if (this->parent_ == nullptr) + return; + this->parent_->stop(); +} + +template void Action::play_next(Ts... x) { + if (this->next_ != nullptr) { + this->next_->play(x...); + } +} +template void Action::stop() { this->stop_next(); } +template void Action::stop_next() { + if (this->next_ != nullptr) { + this->next_->stop(); + } +} +template void Action::play_next_tuple(const std::tuple &tuple) { + this->play_next_tuple_(tuple, typename gens::type()); +} +template +template +void Action::play_next_tuple_(const std::tuple &tuple, seq) { + this->play_next(std::get(tuple)...); +} + +template DelayAction::DelayAction() = default; + +template void DelayAction::play(Ts... x) { + auto f = std::bind(&DelayAction::play_next, this, x...); + this->set_timeout(this->delay_.value(x...), f); +} +template float DelayAction::get_setup_priority() const { return setup_priority::HARDWARE; } +template void DelayAction::stop() { + this->cancel_timeout(""); + this->stop_next(); +} + +template Condition *Automation::add_condition(Condition *condition) { + this->conditions_.push_back(condition); + return condition; +} +template void Automation::add_conditions(const std::vector *> &conditions) { + for (auto *condition : conditions) { + this->add_condition(condition); + } +} +template Automation::Automation(Trigger *trigger) : trigger_(trigger) { + this->trigger_->set_parent(this); +} +template Action *Automation::add_action(Action *action) { + this->actions_.add_action(action); +} +template void Automation::add_actions(const std::vector *> &actions) { + this->actions_.add_actions(actions); +} +template void Automation::trigger(Ts... x) { + for (auto *condition : this->conditions_) { + if (!condition->check(x...)) + return; + } + + this->actions_.play(x...); +} +template void Automation::stop() { this->actions_.stop(); } +template LambdaCondition::LambdaCondition(std::function &&f) : f_(std::move(f)) {} +template bool LambdaCondition::check(Ts... x) { return this->f_(x...); } + +template LambdaAction::LambdaAction(std::function &&f) : f_(std::move(f)) {} +template void LambdaAction::play(Ts... x) { + this->f_(x...); + this->play_next(x...); +} + +template Action *ActionList::add_action(Action *action) { + if (this->actions_end_ == nullptr) { + this->actions_begin_ = action; + } else { + this->actions_end_->next_ = action; + } + return this->actions_end_ = action; +} +template void ActionList::add_actions(const std::vector *> &actions) { + for (auto *action : actions) { + this->add_action(action); + } +} +template void ActionList::play(Ts... x) { + if (this->actions_begin_ != nullptr) + this->actions_begin_->play(x...); +} +template void ActionList::stop() { + if (this->actions_begin_ != nullptr) + this->actions_begin_->stop(); +} +template bool ActionList::empty() const { return this->actions_begin_ == nullptr; } +template +IfAction::IfAction(const std::vector *> conditions) : conditions_(conditions) {} +template void IfAction::play(Ts... x) { + bool res = true; + for (auto *condition : this->conditions_) { + if (!condition->check(x...)) { + res = false; + break; + } + } + if (res) { + if (this->then_.empty()) { + this->play_next(x...); + } else { + this->then_.play(x...); + } + } else { + if (this->else_.empty()) { + this->play_next(x...); + } else { + this->else_.play(x...); + } + } +} +template void IfAction::add_then(const std::vector *> &actions) { + this->then_.add_actions(actions); + this->then_.add_action(new LambdaAction([this](Ts... x) { this->play_next(x...); })); +} +template void IfAction::add_else(const std::vector *> &actions) { + this->else_.add_actions(actions); + this->else_.add_action(new LambdaAction([this](Ts... x) { this->play_next(x...); })); +} +template void IfAction::stop() { + this->then_.stop(); + this->else_.stop(); + this->stop_next(); +} + +template void UpdateComponentAction::play(Ts... x) { + this->component_->update(); + this->play_next(x...); +} + +template +UpdateComponentAction::UpdateComponentAction(PollingComponent *component) : component_(component) {} + +template +WhileAction::WhileAction(const std::vector *> &conditions) : conditions_(conditions) {} + +template void WhileAction::add_then(const std::vector *> &actions) { + this->then_.add_actions(actions); + this->then_.add_action(new LambdaAction([this](Ts... x) { + this->is_running_ = false; + this->play(x...); + })); +} +template void WhileAction::play(Ts... x) { + if (this->is_running_) + return; + + for (auto *condition : this->conditions_) { + if (!condition->check(x...)) { + this->play_next(x...); + return; + } + } + this->is_running_ = true; + this->then_.play(x...); +} +template void WhileAction::stop() { + this->then_.stop(); + this->is_running_ = false; + this->stop_next(); +} + +template +WaitUntilAction::WaitUntilAction(const std::vector *> &conditions) : conditions_(conditions) {} +template void WaitUntilAction::play(Ts... x) { + this->var_ = std::make_tuple(x...); + this->triggered_ = true; + this->loop(); +} +template void WaitUntilAction::stop() { + this->triggered_ = false; + this->stop_next(); +} +template void WaitUntilAction::loop() { + if (!this->triggered_) + return; + + for (auto *condition : this->conditions_) { + if (!condition->check_tuple(this->var_)) { + return; + } + } + + this->triggered_ = false; + this->play_next_tuple(this->var_); +} +template float WaitUntilAction::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace esphome diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp new file mode 100644 index 000000000..fbd7439d7 --- /dev/null +++ b/esphome/core/component.cpp @@ -0,0 +1,251 @@ +#include + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/esphal.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { + +static const char *TAG = "component"; + +namespace setup_priority { + +const float BUS = 1000.0f; +const float IO = 900.0f; +const float HARDWARE = 800.0f; +const float DATA = 600.0f; +const float PROCESSOR = 400.0; +const float WIFI = 250.0f; +const float AFTER_WIFI = 200.0f; +const float AFTER_CONNECTION = 100.0f; +const float LATE = -100.0f; + +} // namespace setup_priority + +const uint32_t COMPONENT_STATE_MASK = 0xFF; +const uint32_t COMPONENT_STATE_CONSTRUCTION = 0x00; +const uint32_t COMPONENT_STATE_SETUP = 0x01; +const uint32_t COMPONENT_STATE_LOOP = 0x02; +const uint32_t COMPONENT_STATE_FAILED = 0x03; +const uint32_t STATUS_LED_MASK = 0xFF00; +const uint32_t STATUS_LED_OK = 0x0000; +const uint32_t STATUS_LED_WARNING = 0x0100; +const uint32_t STATUS_LED_ERROR = 0x0200; + +uint32_t global_state = 0; + +float Component::get_loop_priority() const { return 0.0f; } + +float Component::get_setup_priority() const { return setup_priority::DATA; } + +void Component::setup() {} + +void Component::loop() {} + +void Component::set_interval(const std::string &name, uint32_t interval, std::function &&f) { // NOLINT + const uint32_t now = millis(); + // only put offset in lower half + uint32_t offset = 0; + if (interval != 0) + offset = (random_uint32() % interval) / 2; + ESP_LOGVV(TAG, "set_interval(name='%s', interval=%u, offset=%u)", name.c_str(), interval, offset); + + if (!name.empty()) { + this->cancel_interval(name); + } + struct TimeFunction function = { + .name = name, + .type = TimeFunction::INTERVAL, + .interval = interval, + .last_execution = now - interval - offset, + .f = std::move(f), + .remove = false, + }; + this->time_functions_.push_back(function); +} + +bool Component::cancel_interval(const std::string &name) { // NOLINT + return this->cancel_time_function_(name, TimeFunction::INTERVAL); +} + +void Component::set_timeout(const std::string &name, uint32_t timeout, std::function &&f) { // NOLINT + const uint32_t now = millis(); + ESP_LOGVV(TAG, "set_timeout(name='%s', timeout=%u)", name.c_str(), timeout); + + if (!name.empty()) { + this->cancel_timeout(name); + } + struct TimeFunction function = { + .name = name, + .type = TimeFunction::TIMEOUT, + .interval = timeout, + .last_execution = now, + .f = std::move(f), + .remove = false, + }; + this->time_functions_.push_back(function); +} + +bool Component::cancel_timeout(const std::string &name) { // NOLINT + return this->cancel_time_function_(name, TimeFunction::TIMEOUT); +} + +void Component::call_loop() { + this->loop_internal_(); + this->loop(); +} + +bool Component::cancel_time_function_(const std::string &name, TimeFunction::Type type) { + // NOLINTNEXTLINE + for (auto iter = this->time_functions_.begin(); iter != this->time_functions_.end(); iter++) { + if (!iter->remove && iter->name == name && iter->type == type) { + ESP_LOGVV(TAG, "Removing old time function %s.", iter->name.c_str()); + iter->remove = true; + return true; + } + } + return false; +} +void Component::call_setup() { + this->setup_internal_(); + this->setup(); +} +uint32_t Component::get_component_state() const { return this->component_state_; } +void Component::loop_internal_() { + this->component_state_ &= ~COMPONENT_STATE_MASK; + this->component_state_ |= COMPONENT_STATE_LOOP; + + for (unsigned int i = 0; i < this->time_functions_.size(); i++) { // NOLINT + const uint32_t now = millis(); + TimeFunction *tf = &this->time_functions_[i]; + if (tf->should_run(now)) { +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + const char *type = + tf->type == TimeFunction::INTERVAL ? "interval" : (tf->type == TimeFunction::TIMEOUT ? "timeout" : "defer"); + ESP_LOGVV(TAG, "Running %s '%s':%u with interval=%u last_execution=%u (now=%u)", type, tf->name.c_str(), i, + tf->interval, tf->last_execution, now); +#endif + + tf->f(); + // The vector might have reallocated due to new items + tf = &this->time_functions_[i]; + + if (tf->type == TimeFunction::INTERVAL && tf->interval != 0) { + const uint32_t amount = (now - tf->last_execution) / tf->interval; + tf->last_execution += (amount * tf->interval); + } else if (tf->type == TimeFunction::DEFER || tf->type == TimeFunction::TIMEOUT) { + tf->remove = true; + } + } + } + + this->time_functions_.erase(std::remove_if(this->time_functions_.begin(), this->time_functions_.end(), + [](const TimeFunction &tf) -> bool { return tf.remove; }), + this->time_functions_.end()); +} +void Component::setup_internal_() { + this->component_state_ &= ~COMPONENT_STATE_MASK; + this->component_state_ |= COMPONENT_STATE_SETUP; +} +void Component::mark_failed() { + ESP_LOGE(TAG, "Component was marked as failed."); + this->component_state_ &= ~COMPONENT_STATE_MASK; + this->component_state_ |= COMPONENT_STATE_FAILED; + this->status_set_error(); +} +void Component::defer(std::function &&f) { this->defer("", std::move(f)); } // NOLINT +bool Component::cancel_defer(const std::string &name) { // NOLINT + return this->cancel_time_function_(name, TimeFunction::DEFER); +} +void Component::defer(const std::string &name, std::function &&f) { // NOLINT + if (!name.empty()) { + this->cancel_defer(name); + } + struct TimeFunction function = { + .name = name, + .type = TimeFunction::DEFER, + .interval = 0, + .last_execution = 0, + .f = std::move(f), + .remove = false, + }; + this->time_functions_.push_back(function); +} +void Component::set_timeout(uint32_t timeout, std::function &&f) { // NOLINT + this->set_timeout("", timeout, std::move(f)); +} +void Component::set_interval(uint32_t interval, std::function &&f) { // NOLINT + this->set_interval("", interval, std::move(f)); +} +bool Component::is_failed() { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } +bool Component::can_proceed() { return true; } +bool Component::status_has_warning() { return this->component_state_ & STATUS_LED_WARNING; } +bool Component::status_has_error() { return this->component_state_ & STATUS_LED_ERROR; } +void Component::status_set_warning() { + this->component_state_ |= STATUS_LED_WARNING; + App.app_state_ |= STATUS_LED_WARNING; +} +void Component::status_set_error() { + this->component_state_ |= STATUS_LED_ERROR; + App.app_state_ |= STATUS_LED_ERROR; +} +void Component::status_clear_warning() { this->component_state_ &= ~STATUS_LED_WARNING; } +void Component::status_clear_error() { this->component_state_ &= ~STATUS_LED_ERROR; } +void Component::status_momentary_warning(const std::string &name, uint32_t length) { + this->status_set_warning(); + this->set_timeout(name, length, [this]() { this->status_clear_warning(); }); +} +void Component::status_momentary_error(const std::string &name, uint32_t length) { + this->status_set_error(); + this->set_timeout(name, length, [this]() { this->status_clear_error(); }); +} +void Component::dump_config() {} +float Component::get_actual_setup_priority() const { + return this->setup_priority_override_.value_or(this->get_setup_priority()); +} +void Component::set_setup_priority(float priority) { this->setup_priority_override_ = priority; } + +PollingComponent::PollingComponent(uint32_t update_interval) : Component(), update_interval_(update_interval) {} + +void PollingComponent::call_setup() { + // Call component internal setup. + this->setup_internal_(); + + // Let the polling component subclass setup their HW. + this->setup(); + + // Register interval. + this->set_interval("update", this->get_update_interval(), [this]() { this->update(); }); +} + +uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; } +void PollingComponent::set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } + +const std::string &Nameable::get_name() const { return this->name_; } +void Nameable::set_name(const std::string &name) { + this->name_ = name; + this->calc_object_id_(); +} +Nameable::Nameable(const std::string &name) : name_(name) { this->calc_object_id_(); } + +const std::string &Nameable::get_object_id() { return this->object_id_; } +bool Nameable::is_internal() const { return this->internal_; } +void Nameable::set_internal(bool internal) { this->internal_ = internal; } +void Nameable::calc_object_id_() { + this->object_id_ = sanitize_string_whitelist(to_lowercase_underscore(this->name_), HOSTNAME_CHARACTER_WHITELIST); + // FNV-1 hash + this->object_id_hash_ = fnv1_hash(this->object_id_); +} +uint32_t Nameable::get_object_id_hash() { return this->object_id_hash_; } + +bool Component::TimeFunction::should_run(uint32_t now) const { + if (this->remove) + return false; + if (this->type == DEFER) + return true; + return this->interval != 4294967295UL && now - this->last_execution > this->interval; +} + +} // namespace esphome diff --git a/esphome/core/component.h b/esphome/core/component.h new file mode 100644 index 000000000..57dd1c08c --- /dev/null +++ b/esphome/core/component.h @@ -0,0 +1,298 @@ +#pragma once + +#include +#include +#include + +#include "esphome/core/optional.h" + +namespace esphome { + +/** Default setup priorities for components of different types. + * + * Components should return one of these setup priorities in get_setup_priority. + */ +namespace setup_priority { + +/// For communication buses like i2c/spi +extern const float BUS; +/// For components that represent GPIO pins like PCF8573 +extern const float IO; +/// For components that deal with hardware and are very important like GPIO switch +extern const float HARDWARE; +/// For components that import data from directly connected sensors like DHT. +extern const float DATA; +/// Alias for DATA (here for compatability reasons) +extern const float HARDWARE_LATE; +/// For components that use data from sensors like displays +extern const float PROCESSOR; +extern const float WIFI; +/// For components that should be initialized after WiFi is connected. +extern const float AFTER_WIFI; +/// For components that should be initialized after a data connection (API/MQTT) is connected. +extern const float AFTER_CONNECTION; +/// For components that should be initialized at the very end of the setup process. +extern const float LATE; + +} // namespace setup_priority + +#define LOG_UPDATE_INTERVAL(this) \ + if (this->get_update_interval() < 100) { \ + ESP_LOGCONFIG(TAG, " Update Interval: %.3fs", this->get_update_interval() / 1000.0f); \ + } else { \ + ESP_LOGCONFIG(TAG, " Update Interval: %.1fs", this->get_update_interval() / 1000.0f); \ + } + +extern const uint32_t COMPONENT_STATE_MASK; +extern const uint32_t COMPONENT_STATE_CONSTRUCTION; +extern const uint32_t COMPONENT_STATE_SETUP; +extern const uint32_t COMPONENT_STATE_LOOP; +extern const uint32_t COMPONENT_STATE_FAILED; +extern const uint32_t STATUS_LED_MASK; +extern const uint32_t STATUS_LED_OK; +extern const uint32_t STATUS_LED_WARNING; +extern const uint32_t STATUS_LED_ERROR; + +class Component { + public: + /** Where the component's initialization should happen. + * + * Analogous to Arduino's setup(). This method is guaranteed to only be called once. + * Defaults to doing nothing. + */ + virtual void setup(); + + /** This method will be called repeatedly. + * + * Analogous to Arduino's loop(). setup() is guaranteed to be called before this. + * Defaults to doing nothing. + */ + virtual void loop(); + + virtual void dump_config(); + + /** priority of setup(). higher -> executed earlier + * + * Defaults to 0. + * + * @return The setup priority of this component + */ + virtual float get_setup_priority() const; + + float get_actual_setup_priority() const; + + void set_setup_priority(float priority); + + /** priority of loop(). higher -> executed earlier + * + * Defaults to 0. + * + * @return The loop priority of this component + */ + virtual float get_loop_priority() const; + + /** Public loop() functions. These will be called by the Application instance. + * + * Note: This should normally not be overriden, unless you know what you're doing. + * They're basically to make creating custom components easier. For example the + * SensorComponent can override these methods to not have the user call some super + * methods within their custom sensors. These methods should ALWAYS call the loop_internal() + * and setup_internal() methods. + * + * Basically, it handles stuff like interval/timeout functions and eventually calls loop(). + */ + virtual void call_loop(); + virtual void call_setup(); + + virtual void on_shutdown() {} + virtual void on_safe_shutdown() {} + + uint32_t get_component_state() const; + + /** Mark this component as failed. Any future timeouts/intervals/setup/loop will no longer be called. + * + * This might be useful if a component wants to indicate that a connection to its peripheral failed. + * For example, i2c based components can check if the remote device is responding and otherwise + * mark the component as failed. Eventually this will also enable smart status LEDs. + */ + virtual void mark_failed(); + + bool is_failed(); + + virtual bool can_proceed(); + + bool status_has_warning(); + + bool status_has_error(); + + void status_set_warning(); + + void status_set_error(); + + void status_clear_warning(); + + void status_clear_error(); + + void status_momentary_warning(const std::string &name, uint32_t length = 5000); + + void status_momentary_error(const std::string &name, uint32_t length = 5000); + + protected: + /** Set an interval function with a unique name. Empty name means no cancelling possible. + * + * This will call f every interval ms. Can be cancelled via CancelInterval(). + * Similar to javascript's setInterval(). + * + * IMPORTANT: Do not rely on this having correct timing. This is only called from + * loop() and therefore can be significantly delay. If you need exact timing please + * use hardware timers. + * + * @param name The identifier for this interval function. + * @param interval The interval in ms. + * @param f The function (or lambda) that should be called + * + * @see cancel_interval() + */ + void set_interval(const std::string &name, uint32_t interval, std::function &&f); // NOLINT + + void set_interval(uint32_t interval, std::function &&f); // NOLINT + + /** Cancel an interval function. + * + * @param name The identifier for this interval function. + * @return Whether an interval functions was deleted. + */ + bool cancel_interval(const std::string &name); // NOLINT + + void set_timeout(uint32_t timeout, std::function &&f); // NOLINT + + /** Set a timeout function with a unique name. + * + * Similar to javascript's setTimeout(). Empty name means no cancelling possible. + * + * IMPORTANT: Do not rely on this having correct timing. This is only called from + * loop() and therefore can be significantly delay. If you need exact timing please + * use hardware timers. + * + * @param name The identifier for this timeout function. + * @param timeout The timeout in ms. + * @param f The function (or lambda) that should be called + * + * @see cancel_timeout() + */ + void set_timeout(const std::string &name, uint32_t timeout, std::function &&f); // NOLINT + + /** Cancel a timeout function. + * + * @param name The identifier for this timeout function. + * @return Whether a timeout functions was deleted. + */ + bool cancel_timeout(const std::string &name); // NOLINT + + /** Defer a callback to the next loop() call. + * + * If name is specified and a defer() object with the same name exists, the old one is first removed. + * + * @param name The name of the defer function. + * @param f The callback. + */ + void defer(const std::string &name, std::function &&f); // NOLINT + + /// Defer a callback to the next loop() call. + void defer(std::function &&f); // NOLINT + + /// Cancel a defer callback using the specified name, name must not be empty. + bool cancel_defer(const std::string &name); // NOLINT + + void loop_internal_(); + void setup_internal_(); + + /// Internal struct for storing timeout/interval functions. + struct TimeFunction { + std::string name; ///< The name/id of this TimeFunction. + enum Type { TIMEOUT, INTERVAL, DEFER } type; ///< The type of this TimeFunction. Either TIMEOUT, INTERVAL or DEFER. + uint32_t interval; ///< The interval/timeout of this function. + /// The last execution for interval functions and the time, SetInterval was called, for timeout functions. + uint32_t last_execution; + std::function f; ///< The function (or callback) itself. + bool remove; + + bool should_run(uint32_t now) const; + }; + + /// Cancel an only time function. If name is empty, won't do anything. + bool cancel_time_function_(const std::string &name, TimeFunction::Type type); + + /** Storage for interval/timeout functions. + * + * Intentionally a vector despite its map-like nature, because of the + * memory overhead. + */ + std::vector time_functions_; + + uint32_t component_state_{0x0000}; ///< State of this component. + optional setup_priority_override_; +}; + +/** This class simplifies creating components that periodically check a state. + * + * You basically just need to implement the update() function, it will be called every update_interval ms + * after startup. Note that this class cannot guarantee a correct timing, as it's not using timers, just + * a software polling feature with set_interval() from Component. + */ +class PollingComponent : public Component { + public: + /** Initialize this polling component with the given update interval in ms. + * + * @param update_interval The update interval in ms. + */ + explicit PollingComponent(uint32_t update_interval); + + /** Manually set the update interval in ms for this polling object. + * + * Override this if you want to do some validation for the update interval. + * + * @param update_interval The update interval in ms. + */ + virtual void set_update_interval(uint32_t update_interval); + + // ========== OVERRIDE METHODS ========== + // (You'll only need this when creating your own custom sensor) + virtual void update() = 0; + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + void call_setup() override; + + /// Get the update interval in ms of this sensor + virtual uint32_t get_update_interval() const; + + protected: + uint32_t update_interval_; +}; + +/// Helper class that enables naming of objects so that it doesn't have to be re-implement every time. +class Nameable { + public: + explicit Nameable(const std::string &name); + const std::string &get_name() const; + void set_name(const std::string &name); + /// Get the sanitized name of this nameable as an ID. Caching it internally. + const std::string &get_object_id(); + uint32_t get_object_id_hash(); + + bool is_internal() const; + void set_internal(bool internal); + + protected: + virtual uint32_t hash_base() = 0; + + void calc_object_id_(); + + std::string name_; + std::string object_id_; + uint32_t object_id_hash_; + bool internal_{false}; +}; + +} // namespace esphome diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp new file mode 100644 index 000000000..bd68d777f --- /dev/null +++ b/esphome/core/controller.cpp @@ -0,0 +1,58 @@ +#include "controller.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { + +void Controller::setup_controller() { +#ifdef USE_BINARY_SENSOR + for (auto *obj : App.get_binary_sensors()) { + if (!obj->is_internal()) + obj->add_on_state_callback([this, obj](bool state) { this->on_binary_sensor_update(obj, state); }); + } +#endif +#ifdef USE_FAN + for (auto *obj : App.get_fans()) { + if (!obj->is_internal()) + obj->add_on_state_callback([this, obj]() { this->on_fan_update(obj); }); + } +#endif +#ifdef USE_LIGHT + for (auto *obj : App.get_lights()) { + if (!obj->is_internal()) + obj->add_new_remote_values_callback([this, obj]() { this->on_light_update(obj); }); + } +#endif +#ifdef USE_SENSOR + for (auto *obj : App.get_sensors()) { + if (!obj->is_internal()) + obj->add_on_state_callback([this, obj](float state) { this->on_sensor_update(obj, state); }); + } +#endif +#ifdef USE_SWITCH + for (auto *obj : App.get_switches()) { + if (!obj->is_internal()) + obj->add_on_state_callback([this, obj](bool state) { this->on_switch_update(obj, state); }); + } +#endif +#ifdef USE_COVER + for (auto *obj : App.get_covers()) { + if (!obj->is_internal()) + obj->add_on_state_callback([this, obj]() { this->on_cover_update(obj); }); + } +#endif +#ifdef USE_TEXT_SENSOR + for (auto *obj : App.get_text_sensors()) { + if (!obj->is_internal()) + obj->add_on_state_callback([this, obj](std::string state) { this->on_text_sensor_update(obj, state); }); + } +#endif +#ifdef USE_CLIMATE + for (auto *obj : App.get_climates()) { + if (!obj->is_internal()) + obj->add_on_state_callback([this, obj]() { this->on_climate_update(obj); }); + } +#endif +} + +} // namespace esphome diff --git a/esphome/core/controller.h b/esphome/core/controller.h new file mode 100644 index 000000000..fa7d1f2ef --- /dev/null +++ b/esphome/core/controller.h @@ -0,0 +1,60 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#ifdef USE_FAN +#include "esphome/components/fan/fan_state.h" +#endif +#ifdef USE_LIGHT +#include "esphome/components/light/light_state.h" +#endif +#ifdef USE_COVER +#include "esphome/components/cover/cover.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 +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif +#ifdef USE_CLIMATE +#include "esphome/components/climate/climate.h" +#endif + +namespace esphome { + +class Controller { + public: + void setup_controller(); +#ifdef USE_BINARY_SENSOR + virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state){}; +#endif +#ifdef USE_FAN + virtual void on_fan_update(fan::FanState *obj){}; +#endif +#ifdef USE_LIGHT + virtual void on_light_update(light::LightState *obj){}; +#endif +#ifdef USE_SENSOR + virtual void on_sensor_update(sensor::Sensor *obj, float state){}; +#endif +#ifdef USE_SWITCH + virtual void on_switch_update(switch_::Switch *obj, bool state){}; +#endif +#ifdef USE_COVER + virtual void on_cover_update(cover::Cover *obj){}; +#endif +#ifdef USE_TEXT_SENSOR + virtual void on_text_sensor_update(text_sensor::TextSensor *obj, std::string state){}; +#endif +#ifdef USE_CLIMATE + virtual void on_climate_update(climate::Climate *obj){}; +#endif +}; + +} // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h new file mode 100644 index 000000000..a587896fa --- /dev/null +++ b/esphome/core/defines.h @@ -0,0 +1,27 @@ +#pragma once +// This file is auto-generated! Do not edit! + +#define ESPHOME_VERSION "dev" + +#define ESPHOME_LOG_LEVEL 6 +#define USE_API +#define USE_LOGGER +#define USE_BINARY_SENSOR +#define USE_SENSOR +#define USE_SWITCH +#define USE_WIFI +#define USE_STATUS_LED +#define USE_TEXT_SENSOR +#define USE_FAN +#define USE_COVER +#define USE_LIGHT +#define USE_CLIMATE +#define USE_MQTT +#define USE_POWER_SUPPLY +#define USE_HOMEASSISTANT_TIME +#define USE_JSON +#ifdef ARDUINO_ARCH_ESP32 +#define USE_ESP32_CAMERA +#endif +#define USE_TIME +#define USE_DEEP_SLEEP diff --git a/esphome/core/esphal.cpp b/esphome/core/esphal.cpp new file mode 100644 index 000000000..f0749894c --- /dev/null +++ b/esphome/core/esphal.cpp @@ -0,0 +1,280 @@ +#include "esphome/core/esphal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/defines.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP8266 +extern "C" { +typedef struct { // NOLINT + void *interruptInfo; // NOLINT + void *functionInfo; // NOLINT +} ArgStructure; + +void ICACHE_RAM_ATTR __attachInterruptArg(uint8_t pin, void (*)(void *), void *fp, // NOLINT + int mode); +}; +#endif + +namespace esphome { + +static const char *TAG = "esphal"; + +GPIOPin::GPIOPin(uint8_t pin, uint8_t mode, bool inverted) + : pin_(pin), + mode_(mode), + inverted_(inverted), +#ifdef ARDUINO_ARCH_ESP8266 + gpio_read_(pin < 16 ? &GPI : &GP16I), + gpio_mask_(pin < 16 ? (1UL << pin) : 1) +#endif +#ifdef ARDUINO_ARCH_ESP32 + gpio_set_(pin < 32 ? &GPIO.out_w1ts : &GPIO.out1_w1ts.val), + gpio_clear_(pin < 32 ? &GPIO.out_w1tc : &GPIO.out1_w1tc.val), + gpio_read_(pin < 32 ? &GPIO.in : &GPIO.in1.val), + gpio_mask_(pin < 32 ? (1UL << pin) : (1UL << (pin - 32))) +#endif +{ +} + +const char *GPIOPin::get_pin_mode_name() const { + const char *mode_s; + switch (this->mode_) { + case INPUT: + mode_s = "INPUT"; + break; + case OUTPUT: + mode_s = "OUTPUT"; + break; + case INPUT_PULLUP: + mode_s = "INPUT_PULLUP"; + break; + case OUTPUT_OPEN_DRAIN: + mode_s = "OUTPUT_OPEN_DRAIN"; + break; + case SPECIAL: + mode_s = "SPECIAL"; + break; + case FUNCTION_1: + mode_s = "FUNCTION_1"; + break; + case FUNCTION_2: + mode_s = "FUNCTION_2"; + break; + case FUNCTION_3: + mode_s = "FUNCTION_3"; + break; + case FUNCTION_4: + mode_s = "FUNCTION_4"; + break; + +#ifdef ARDUINO_ARCH_ESP32 + case PULLUP: + mode_s = "PULLUP"; + break; + case PULLDOWN: + mode_s = "PULLDOWN"; + break; + case INPUT_PULLDOWN: + mode_s = "INPUT_PULLDOWN"; + break; + case OPEN_DRAIN: + mode_s = "OPEN_DRAIN"; + break; + case FUNCTION_5: + mode_s = "FUNCTION_5"; + break; + case FUNCTION_6: + mode_s = "FUNCTION_6"; + break; + case ANALOG: + mode_s = "ANALOG"; + break; +#endif +#ifdef ARDUINO_ARCH_ESP8266 + case FUNCTION_0: + mode_s = "FUNCTION_0"; + break; + case WAKEUP_PULLUP: + mode_s = "WAKEUP_PULLUP"; + break; + case WAKEUP_PULLDOWN: + mode_s = "WAKEUP_PULLDOWN"; + break; + case INPUT_PULLDOWN_16: + mode_s = "INPUT_PULLDOWN_16"; + break; +#endif + + default: + mode_s = "UNKNOWN"; + break; + } + + return mode_s; +} + +unsigned char GPIOPin::get_pin() const { return this->pin_; } +unsigned char GPIOPin::get_mode() const { return this->mode_; } + +bool GPIOPin::is_inverted() const { return this->inverted_; } +void GPIOPin::setup() { this->pin_mode(this->mode_); } +bool ICACHE_RAM_ATTR HOT GPIOPin::digital_read() { + return bool((*this->gpio_read_) & this->gpio_mask_) != this->inverted_; +} +bool ICACHE_RAM_ATTR HOT ISRInternalGPIOPin::digital_read() { + return bool((*this->gpio_read_) & this->gpio_mask_) != this->inverted_; +} +void ICACHE_RAM_ATTR HOT GPIOPin::digital_write(bool value) { +#ifdef ARDUINO_ARCH_ESP8266 + if (this->pin_ != 16) { + if (value != this->inverted_) { + GPOS = this->gpio_mask_; + } else { + GPOC = this->gpio_mask_; + } + } else { + if (value != this->inverted_) { + GP16O |= 1; + } else { + GP16O &= ~1; + } + } +#endif +#ifdef ARDUINO_ARCH_ESP32 + if (value != this->inverted_) { + (*this->gpio_set_) = this->gpio_mask_; + } else { + (*this->gpio_clear_) = this->gpio_mask_; + } +#endif +} +void ISRInternalGPIOPin::digital_write(bool value) { +#ifdef ARDUINO_ARCH_ESP8266 + if (this->pin_ != 16) { + if (value != this->inverted_) { + GPOS = this->gpio_mask_; + } else { + GPOC = this->gpio_mask_; + } + } else { + if (value != this->inverted_) { + GP16O |= 1; + } else { + GP16O &= ~1; + } + } +#endif +#ifdef ARDUINO_ARCH_ESP32 + if (value != this->inverted_) { + (*this->gpio_set_) = this->gpio_mask_; + } else { + (*this->gpio_clear_) = this->gpio_mask_; + } +#endif +} +ISRInternalGPIOPin::ISRInternalGPIOPin(uint8_t pin, +#ifdef ARDUINO_ARCH_ESP32 + volatile uint32_t *gpio_clear, volatile uint32_t *gpio_set, +#endif + volatile uint32_t *gpio_read, uint32_t gpio_mask, bool inverted) + : pin_(pin), + inverted_(inverted), + gpio_read_(gpio_read), + gpio_mask_(gpio_mask) +#ifdef ARDUINO_ARCH_ESP32 + , + gpio_clear_(gpio_clear), + gpio_set_(gpio_set) +#endif +{ +} +void ICACHE_RAM_ATTR ISRInternalGPIOPin::clear_interrupt() { +#ifdef ARDUINO_ARCH_ESP8266 + GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, this->gpio_mask_); +#endif +#ifdef ARDUINO_ARCH_ESP32 + if (this->pin_ < 32) { + GPIO.status_w1tc = this->gpio_mask_; + } else { + GPIO.status1_w1tc.intr_st = this->gpio_mask_; + } +#endif +} + +void ICACHE_RAM_ATTR HOT GPIOPin::pin_mode(uint8_t mode) { +#ifdef ARDUINO_ARCH_ESP8266 + if (this->pin_ == 16 && mode == INPUT_PULLUP) { + // pullups are not available on GPIO16, manually override with + // input mode. + pinMode(16, INPUT); + return; + } +#endif + pinMode(this->pin_, mode); +} + +#ifdef ARDUINO_ARCH_ESP8266 +struct ESPHomeInterruptFuncInfo { + void (*func)(void *); + void *arg; +}; + +void ICACHE_RAM_ATTR interrupt_handler(void *arg) { + ArgStructure *as = static_cast(arg); + auto *info = static_cast(as->functionInfo); + info->func(info->arg); +} +#endif + +void GPIOPin::attach_interrupt_(void (*func)(void *), void *arg, int mode) const { + if (this->inverted_) { + if (mode == RISING) { + mode = FALLING; + } else if (mode == FALLING) { + mode = RISING; + } + } +#ifdef ARDUINO_ARCH_ESP8266 + ArgStructure *as = new ArgStructure; + as->interruptInfo = nullptr; + + as->functionInfo = new ESPHomeInterruptFuncInfo{ + .func = func, + .arg = arg, + }; + + __attachInterruptArg(this->pin_, interrupt_handler, as, mode); +#endif +#ifdef ARDUINO_ARCH_ESP32 + // work around issue https://github.com/espressif/arduino-esp32/pull/1776 in arduino core + // yet again proves how horrible code is there :( - how could that have been accepted... + auto *attach = reinterpret_cast(attachInterruptArg); + attach(this->pin_, func, arg, mode); +#endif +} + +ISRInternalGPIOPin *GPIOPin::to_isr() const { + return new ISRInternalGPIOPin(this->pin_, +#ifdef ARDUINO_ARCH_ESP32 + this->gpio_clear_, this->gpio_set_, +#endif + this->gpio_read_, this->gpio_mask_, this->inverted_); +} + +} // namespace esphome + +#ifdef ARDUINO_ESP8266_RELEASE_2_3_0 +// Fix 2.3.0 std missing memchr +extern "C" { +void *memchr(const void *s, int c, size_t n) { + if (n == 0) + return nullptr; + const uint8_t *p = reinterpret_cast(s); + do { + if (*p++ == c) + return const_cast(reinterpret_cast(p - 1)); + } while (--n != 0); + return nullptr; +} +}; +#endif diff --git a/esphome/core/esphal.h b/esphome/core/esphal.h new file mode 100644 index 000000000..493f7f5e3 --- /dev/null +++ b/esphome/core/esphal.h @@ -0,0 +1,118 @@ +#pragma once + +#include "Arduino.h" +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif +// Fix some arduino defs +#ifdef round +#undef round +#endif +#ifdef bool +#undef bool +#endif +#ifdef true +#undef true +#endif +#ifdef false +#undef false +#endif +#ifdef min +#undef min +#endif +#ifdef max +#undef max +#endif +#ifdef abs +#undef abs +#endif + +namespace esphome { + +#define LOG_PIN(prefix, pin) \ + if ((pin) != nullptr) { \ + ESP_LOGCONFIG(TAG, prefix LOG_PIN_PATTERN, LOG_PIN_ARGS(pin)); \ + } +#define LOG_PIN_PATTERN "GPIO%u (Mode: %s%s)" +#define LOG_PIN_ARGS(pin) (pin)->get_pin(), (pin)->get_pin_mode_name(), ((pin)->is_inverted() ? ", INVERTED" : "") + +/// Copy of GPIOPin that is safe to use from ISRs (with no virtual functions) +class ISRInternalGPIOPin { + public: + ISRInternalGPIOPin(uint8_t pin, +#ifdef ARDUINO_ARCH_ESP32 + volatile uint32_t *gpio_clear, volatile uint32_t *gpio_set, +#endif + volatile uint32_t *gpio_read, uint32_t gpio_mask, bool inverted); + bool digital_read(); + void digital_write(bool value); + void clear_interrupt(); + + protected: + const uint8_t pin_; + const bool inverted_; + volatile uint32_t *const gpio_read_; + const uint32_t gpio_mask_; +#ifdef ARDUINO_ARCH_ESP32 + volatile uint32_t *const gpio_clear_; + volatile uint32_t *const gpio_set_; +#endif +}; + +/** A high-level abstraction class that can expose a pin together with useful options like pinMode. + * + * Set the parameters for this at construction time and use setup() to apply them. The inverted parameter will + * automatically invert the input/output for you. + * + * Use read_value() and write_value() to use digitalRead() and digitalWrite(), respectively. + */ +class GPIOPin { + public: + /** Construct the GPIOPin instance. + * + * @param pin The GPIO pin number of this instance. + * @param mode The Arduino pinMode that this pin should be put into at setup(). + * @param inverted Whether all digitalRead/digitalWrite calls should be inverted. + */ + GPIOPin(uint8_t pin, uint8_t mode, bool inverted = false); + + /// Setup the pin mode. + virtual void setup(); + /// Read the binary value from this pin using digitalRead (and inverts automatically). + virtual bool digital_read(); + /// Write the binary value to this pin using digitalWrite (and inverts automatically). + virtual void digital_write(bool value); + /// Set the pin mode + virtual void pin_mode(uint8_t mode); + + /// Get the GPIO pin number. + uint8_t get_pin() const; + const char *get_pin_mode_name() const; + /// Get the pinMode of this pin. + uint8_t get_mode() const; + /// Return whether this pin shall be treated as inverted. (for example active-low) + bool is_inverted() const; + + template void attach_interrupt(void (*func)(T *), T *arg, int mode) const; + + ISRInternalGPIOPin *to_isr() const; + + protected: + void attach_interrupt_(void (*func)(void *), void *arg, int mode) const; + + const uint8_t pin_; + const uint8_t mode_; + const bool inverted_; +#ifdef ARDUINO_ARCH_ESP32 + volatile uint32_t *const gpio_set_; + volatile uint32_t *const gpio_clear_; +#endif + volatile uint32_t *const gpio_read_; + const uint32_t gpio_mask_; +}; + +template void GPIOPin::attach_interrupt(void (*func)(T *), T *arg, int mode) const { + this->attach_interrupt_(reinterpret_cast(func), arg, mode); +} + +} // namespace esphome diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp new file mode 100644 index 000000000..184e8eef2 --- /dev/null +++ b/esphome/core/helpers.cpp @@ -0,0 +1,307 @@ +#include "esphome/core/helpers.h" +#include +#include + +#ifdef ARDUINO_ARCH_ESP8266 +#include +#else +#include +#endif + +#include "esphome/core/log.h" +#include "esphome/core/esphal.h" + +namespace esphome { + +static const char *TAG = "helpers"; + +std::string get_mac_address() { + char tmp[20]; + uint8_t mac[6]; +#ifdef ARDUINO_ARCH_ESP32 + esp_efuse_mac_get_default(mac); +#endif +#ifdef ARDUINO_ARCH_ESP8266 + WiFi.macAddress(mac); +#endif + sprintf(tmp, "%02x%02x%02x%02x%02x%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + return std::string(tmp); +} + +std::string get_mac_address_pretty() { + char tmp[20]; + uint8_t mac[6]; +#ifdef ARDUINO_ARCH_ESP32 + esp_efuse_mac_get_default(mac); +#endif +#ifdef ARDUINO_ARCH_ESP8266 + WiFi.macAddress(mac); +#endif + sprintf(tmp, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + return std::string(tmp); +} + +std::string generate_hostname(const std::string &base) { return base + std::string("-") + get_mac_address(); } + +uint32_t random_uint32() { +#ifdef ARDUINO_ARCH_ESP32 + return esp_random(); +#else + return os_random(); +#endif +} + +double random_double() { return random_uint32() / double(UINT32_MAX); } + +float random_float() { return float(random_double()); } + +static uint32_t fast_random_seed = 0; + +void fast_random_set_seed(uint32_t seed) { fast_random_seed = seed; } +uint32_t fast_random_32() { + fast_random_seed = (fast_random_seed * 2654435769ULL) + 40503ULL; + return fast_random_seed; +} +uint16_t fast_random_16() { + uint32_t rand32 = fast_random_32(); + return (rand32 & 0xFFFF) + (rand32 >> 16); +} +uint8_t fast_random_8() { + uint8_t rand32 = fast_random_32(); + return (rand32 & 0xFF) + ((rand32 >> 8) & 0xFF); +} + +float gamma_correct(float value, float gamma) { + if (value <= 0.0f) + return 0.0f; + if (gamma <= 0.0f) + return value; + + return powf(value, gamma); +} +std::string to_lowercase_underscore(std::string s) { + std::transform(s.begin(), s.end(), s.begin(), ::tolower); + std::replace(s.begin(), s.end(), ' ', '_'); + return s; +} + +std::string sanitize_string_whitelist(const std::string &s, const std::string &whitelist) { + std::string out(s); + out.erase(std::remove_if(out.begin(), out.end(), + [&out, &whitelist](const char &c) { return whitelist.find(c) == std::string::npos; }), + out.end()); + return out; +} + +std::string sanitize_hostname(const std::string &hostname) { + std::string s = sanitize_string_whitelist(hostname, HOSTNAME_CHARACTER_WHITELIST); + return truncate_string(s, 63); +} + +std::string truncate_string(const std::string &s, size_t length) { + if (s.length() > length) + return s.substr(0, length); + return s; +} + +std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { + auto multiplier = float(pow10(accuracy_decimals)); + float value_rounded = roundf(value * multiplier) / multiplier; + char tmp[32]; // should be enough, but we should maybe improve this at some point. + dtostrf(value_rounded, 0, uint8_t(std::max(0, int(accuracy_decimals))), tmp); + return std::string(tmp); +} +std::string uint64_to_string(uint64_t num) { + char buffer[17]; + auto *address16 = reinterpret_cast(&num); + snprintf(buffer, sizeof(buffer), "%04X%04X%04X%04X", address16[3], address16[2], address16[1], address16[0]); + return std::string(buffer); +} +std::string uint32_to_string(uint32_t num) { + char buffer[9]; + auto *address16 = reinterpret_cast(&num); + snprintf(buffer, sizeof(buffer), "%04X%04X", address16[1], address16[0]); + return std::string(buffer); +} +static char *global_json_build_buffer = nullptr; +static size_t global_json_build_buffer_size = 0; + +void reserve_global_json_build_buffer(size_t required_size) { + if (global_json_build_buffer_size == 0 || global_json_build_buffer_size < required_size) { + delete[] global_json_build_buffer; + global_json_build_buffer_size = std::max(required_size, global_json_build_buffer_size * 2); + + size_t remainder = global_json_build_buffer_size % 16U; + if (remainder != 0) + global_json_build_buffer_size += 16 - remainder; + + global_json_build_buffer = new char[global_json_build_buffer_size]; + } +} + +ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { + if (on == nullptr && strcasecmp(str, "on") == 0) + return PARSE_ON; + if (on != nullptr && strcasecmp(str, on) == 0) + return PARSE_ON; + if (off == nullptr && strcasecmp(str, "off") == 0) + return PARSE_OFF; + if (off != nullptr && strcasecmp(str, off) == 0) + return PARSE_OFF; + if (strcasecmp(str, "toggle") == 0) + return PARSE_TOGGLE; + + return PARSE_NONE; +} + +const char *HOSTNAME_CHARACTER_WHITELIST = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; + +void disable_interrupts() { +#ifdef ARDUINO_ARCH_ESP32 + portDISABLE_INTERRUPTS(); +#else + noInterrupts(); +#endif +} +void enable_interrupts() { +#ifdef ARDUINO_ARCH_ESP32 + portENABLE_INTERRUPTS(); +#else + interrupts(); +#endif +} + +uint8_t crc8(uint8_t *data, uint8_t len) { + uint8_t crc = 0; + + while ((len--) != 0u) { + uint8_t inbyte = *data++; + for (uint8_t i = 8; i != 0u; i--) { + bool mix = (crc ^ inbyte) & 0x01; + crc >>= 1; + if (mix) + crc ^= 0x8C; + inbyte >>= 1; + } + } + return crc; +} +void delay_microseconds_accurate(uint32_t usec) { + if (usec == 0) + return; + + if (usec <= 16383UL) { + delayMicroseconds(usec); + } else { + delay(usec / 1000UL); + delayMicroseconds(usec % 1000UL); + } +} + +uint8_t reverse_bits_8(uint8_t x) { + x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1); + x = ((x & 0xCC) >> 2) | ((x & 0x33) << 2); + x = ((x & 0xF0) >> 4) | ((x & 0x0F) << 4); + return x; +} + +uint16_t reverse_bits_16(uint16_t x) { + return uint16_t(reverse_bits_8(x & 0xFF) << 8) | uint16_t(reverse_bits_8(x >> 8)); +} +std::string to_string(const std::string &val) { return val; } +std::string to_string(int val) { + char buf[64]; + sprintf(buf, "%d", val); + return buf; +} +std::string to_string(long val) { + char buf[64]; + sprintf(buf, "%ld", val); + return buf; +} +std::string to_string(long long val) { + char buf[64]; + sprintf(buf, "%lld", val); + return buf; +} +std::string to_string(unsigned val) { + char buf[64]; + sprintf(buf, "%u", val); + return buf; +} +std::string to_string(unsigned long val) { + char buf[64]; + sprintf(buf, "%lu", val); + return buf; +} +std::string to_string(unsigned long long val) { + char buf[64]; + sprintf(buf, "%llu", val); + return buf; +} +std::string to_string(float val) { + char buf[64]; + sprintf(buf, "%f", val); + return buf; +} +std::string to_string(double val) { + char buf[64]; + sprintf(buf, "%f", val); + return buf; +} +std::string to_string(long double val) { + char buf[64]; + sprintf(buf, "%Lf", val); + return buf; +} +optional parse_float(const std::string &str) { + char *end; + float value = ::strtof(str.c_str(), &end); + if (end == nullptr) + return {}; + return value; +} +uint32_t fnv1_hash(const std::string &str) { + uint32_t hash = 2166136261UL; + for (char c : str) { + hash *= 16777619UL; + hash ^= c; + } + return hash; +} +bool str_equals_case_insensitive(const std::string &a, const std::string &b) { + return strcasecmp(a.c_str(), b.c_str()) == 0; +} + +template uint32_t reverse_bits(uint32_t x) { + return uint32_t(reverse_bits_16(x & 0xFFFF) << 16) | uint32_t(reverse_bits_16(x >> 16)); +} + +static int high_freq_num_requests = 0; + +void HighFrequencyLoopRequester::start() { + if (this->started_) + return; + high_freq_num_requests++; + this->started_ = true; +} +void HighFrequencyLoopRequester::stop() { + if (!this->started_) + return; + high_freq_num_requests--; + this->started_ = false; +} +bool HighFrequencyLoopRequester::is_high_frequency() { return high_freq_num_requests > 0; } + +float clamp(float val, float min, float max) { + if (min > max) + std::swap(min, max); + if (val < min) + return min; + if (val > max) + return max; + return val; +} +float lerp(float completion, float start, float end) { return start + (end - start) * completion; } + +} // namespace esphome diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h new file mode 100644 index 000000000..babd14ff5 --- /dev/null +++ b/esphome/core/helpers.h @@ -0,0 +1,256 @@ +#pragma once + +#include +#include +#include +#include + +#include "esphome/core/optional.h" +#include "esphome/core/esphal.h" + +#ifdef CLANG_TIDY +#undef ICACHE_RAM_ATTR +#define ICACHE_RAM_ATTR +#undef ICACHE_RODATA_ATTR +#define ICACHE_RODATA_ATTR +#endif + +#define HOT __attribute__((hot)) +#define ESPDEPRECATED(msg) __attribute__((deprecated(msg))) +#define ALWAYS_INLINE __attribute__((always_inline)) + +namespace esphome { + +/// The characters that are allowed in a hostname. +extern const char *HOSTNAME_CHARACTER_WHITELIST; + +/// Gets the MAC address as a string, this can be used as way to identify this ESP. +std::string get_mac_address(); + +std::string get_mac_address_pretty(); + +std::string to_string(const std::string &val); +std::string to_string(int val); +std::string to_string(long val); +std::string to_string(long long val); +std::string to_string(unsigned val); +std::string to_string(unsigned long val); +std::string to_string(unsigned long long val); +std::string to_string(float val); +std::string to_string(double val); +std::string to_string(long double val); +optional parse_float(const std::string &str); + +/// Sanitize the hostname by removing characters that are not in the whitelist and truncating it to 63 chars. +std::string sanitize_hostname(const std::string &hostname); + +/// Truncate a string to a specific length +std::string truncate_string(const std::string &s, size_t length); + +/// Convert the string to lowercase_underscore. +std::string to_lowercase_underscore(std::string s); + +/// Compare string a to string b (ignoring case) and return whether they are equal. +bool str_equals_case_insensitive(const std::string &a, const std::string &b); + +class HighFrequencyLoopRequester { + public: + void start(); + void stop(); + + static bool is_high_frequency(); + + protected: + bool started_{false}; +}; + +/** Clamp the value between min and max. + * + * @param val The value. + * @param min The minimum value. + * @param max The maximum value. + * @return val clamped in between min and max. + */ +float clamp(float val, float min, float max); + +/** Linearly interpolate between end start and end by completion. + * + * @tparam T The input/output typename. + * @param start The start value. + * @param end The end value. + * @param completion The completion. 0 is start value, 1 is end value. + * @return The linearly interpolated value. + */ +float lerp(float completion, float start, float end); + +/// std::make_unique +template std::unique_ptr make_unique(Args &&... args) { + return std::unique_ptr(new T(std::forward(args)...)); +} + +/// Return a random 32 bit unsigned integer. +uint32_t random_uint32(); + +/** Returns a random double between 0 and 1. + * + * Note: This function probably doesn't provide a truly uniform distribution. + */ +double random_double(); + +/// Returns a random float between 0 and 1. Essentially just casts random_double() to a float. +float random_float(); + +void fast_random_set_seed(uint32_t seed); +uint32_t fast_random_32(); +uint16_t fast_random_16(); +uint8_t fast_random_8(); + +/// Applies gamma correction with the provided gamma to value. +float gamma_correct(float value, float gamma); + +/// Create a string from a value and an accuracy in decimals. +std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); + +/// Convert a uint64_t to a hex string +std::string uint64_to_string(uint64_t num); + +/// Convert a uint32_t to a hex string +std::string uint32_to_string(uint32_t num); + +/// Sanitizes the input string with the whitelist. +std::string sanitize_string_whitelist(const std::string &s, const std::string &whitelist); + +uint8_t reverse_bits_8(uint8_t x); +uint16_t reverse_bits_16(uint16_t x); +uint32_t reverse_bits_32(uint32_t x); + +/** Cross-platform method to disable interrupts. + * + * Useful when you need to do some timing-dependent communication. + * + * @see Do not forget to call `enable_interrupts()` again or otherwise things will go very wrong. + */ +void disable_interrupts(); + +/// Cross-platform method to enable interrupts after they have been disabled. +void enable_interrupts(); + +/// Calculate a crc8 of data with the provided data length. +uint8_t crc8(uint8_t *data, uint8_t len); + +enum ParseOnOffState { + PARSE_NONE = 0, + PARSE_ON, + PARSE_OFF, + PARSE_TOGGLE, +}; + +ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const char *off = nullptr); + +// https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971 +template struct seq {}; // NOLINT +template struct gens : gens {}; // NOLINT +template struct gens<0, S...> { using type = seq; }; // NOLINT + +template class CallbackManager; + +/** Simple helper class to allow having multiple subscribers to a signal. + * + * @tparam Ts The arguments for the callback, wrapped in void(). + */ +template class CallbackManager { + public: + /// Add a callback to the internal callback list. + void add(std::function &&callback) { this->callbacks_.push_back(std::move(callback)); } + + /// Call all callbacks in this manager. + void call(Ts... args) { + for (auto &cb : this->callbacks_) + cb(args...); + } + + protected: + std::vector> callbacks_; +}; + +// https://stackoverflow.com/a/37161919/8924614 +template +struct is_callable // NOLINT +{ + template static auto test(U *p) -> decltype((*p)(std::declval()...), void(), std::true_type()); + + template static auto test(...) -> decltype(std::false_type()); + + static constexpr auto value = decltype(test(nullptr))::value; // NOLINT +}; + +template using enable_if_t = typename std::enable_if::type; + +template class TemplatableValue { + public: + TemplatableValue() : type_(EMPTY) {} + + template::value, int> = 0> + TemplatableValue(F value) : type_(VALUE), value_(value) {} + + template::value, int> = 0> + TemplatableValue(F f) : type_(LAMBDA), f_(f) {} + + bool has_value() { return this->type_ != EMPTY; } + + T value(X... x) { + if (this->type_ == LAMBDA) { + return this->f_(x...); + } + // return value also when empty + return this->value_; + } + + optional optional_value(X... x) { + if (!this->has_value()) { + return {}; + } + return this->value(x...); + } + + T value_or(X... x, T default_value) { + if (!this->has_value()) { + return default_value; + } + return this->value(x...); + } + + protected: + enum { + EMPTY, + VALUE, + LAMBDA, + } type_; + + T value_; + std::function f_; +}; + +void delay_microseconds_accurate(uint32_t usec); + +template class Deduplicator { + public: + bool next(T value) { + if (this->has_value_) { + if (this->last_value_ == value) + return false; + } + this->has_value_ = true; + this->last_value_ = value; + return true; + } + bool has_value() const { return this->has_value_; } + + protected: + bool has_value_{false}; + T last_value_{}; +}; + +uint32_t fnv1_hash(const std::string &str); + +} // namespace esphome diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp new file mode 100644 index 000000000..8adaebe5b --- /dev/null +++ b/esphome/core/log.cpp @@ -0,0 +1,67 @@ +#include "esphome/core/log.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif + +namespace esphome { + +int HOT esp_log_printf_(int level, const char *tag, const char *format, ...) { // NOLINT + va_list arg; + va_start(arg, format); + int ret = esp_log_vprintf_(level, tag, format, arg); + va_end(arg); + return ret; +} +#ifdef USE_STORE_LOG_STR_IN_FLASH +int HOT esp_log_printf_(int level, const char *tag, const __FlashStringHelper *format, ...) { + va_list arg; + va_start(arg, format); + int ret = esp_log_vprintf_(level, tag, format, arg); + va_end(arg); + return ret; + return 0; +} +#endif + +int HOT esp_log_vprintf_(int level, const char *tag, const char *format, va_list args) { // NOLINT +#ifdef USE_LOGGER + auto *log = logger::global_logger; + if (log == nullptr) + return 0; + + return log->log_vprintf_(level, tag, format, args); +#else + return 0; +#endif +} + +#ifdef USE_STORE_LOG_STR_IN_FLASH +int HOT esp_log_vprintf_(int level, const char *tag, const __FlashStringHelper *format, va_list args) { // NOLINT +#ifdef USE_LOGGER + auto *log = logger::global_logger; + if (log == nullptr) + return 0; + + return log->log_vprintf_(level, tag, format, args); +#else + return 0; +#endif +} +#endif + +int HOT esp_idf_log_vprintf_(const char *format, va_list args) { // NOLINT +#ifdef USE_LOGGER + auto *log = logger::global_logger; + if (log == nullptr) + return 0; + + return log->log_vprintf_(log->get_global_log_level(), "", format, args); +#else + return 0; +#endif +} + +} // namespace esphome diff --git a/esphome/core/log.h b/esphome/core/log.h new file mode 100644 index 000000000..4e4d178b9 --- /dev/null +++ b/esphome/core/log.h @@ -0,0 +1,173 @@ +#pragma once + +#include +#include +#include +#ifdef USE_STORE_LOG_STR_IN_FLASH +#include "WString.h" +#endif + +// avoid esp-idf redefining our macros +#include "esphome/core/esphal.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include "esp_err.h" +#endif + +namespace esphome { + +#define ESPHOME_LOG_LEVEL_NONE 0 +#define ESPHOME_LOG_LEVEL_ERROR 1 +#define ESPHOME_LOG_LEVEL_WARN 2 +#define ESPHOME_LOG_LEVEL_INFO 3 +#define ESPHOME_LOG_LEVEL_DEBUG 4 +#define ESPHOME_LOG_LEVEL_VERBOSE 5 +#define ESPHOME_LOG_LEVEL_VERY_VERBOSE 6 + +#ifndef ESPHOME_LOG_LEVEL +#define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_DEBUG +#endif + +#define ESPHOME_LOG_COLOR_BLACK "30" +#define ESPHOME_LOG_COLOR_RED "31" // ERROR +#define ESPHOME_LOG_COLOR_GREEN "32" // INFO +#define ESPHOME_LOG_COLOR_YELLOW "33" // WARNING +#define ESPHOME_LOG_COLOR_BLUE "34" +#define ESPHOME_LOG_COLOR_MAGENTA "35" // CONFIG +#define ESPHOME_LOG_COLOR_CYAN "36" // DEBUG +#define ESPHOME_LOG_COLOR_GRAY "37" // VERBOSE +#define ESPHOME_LOG_COLOR_WHITE "38" +#define ESPHOME_LOG_SECRET_BEGIN "\033[5m" +#define ESPHOME_LOG_SECRET_END "\033[6m" +#define LOG_SECRET(x) ESPHOME_LOG_SECRET_BEGIN x ESPHOME_LOG_SECRET_END + +#define ESPHOME_LOG_COLOR(COLOR) "\033[0;" COLOR "m" +#define ESPHOME_LOG_BOLD(COLOR) "\033[1;" COLOR "m" + +#define ESPHOME_LOG_COLOR_E ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED) +#define ESPHOME_LOG_COLOR_W ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_YELLOW) +#define ESPHOME_LOG_COLOR_I ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GREEN) +#define ESPHOME_LOG_COLOR_C ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_MAGENTA) +#define ESPHOME_LOG_COLOR_D ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_CYAN) +#define ESPHOME_LOG_COLOR_V ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GRAY) +#define ESPHOME_LOG_COLOR_VV ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_WHITE) +#define ESPHOME_LOG_RESET_COLOR "\033[0m" + +int esp_log_printf_(int level, const char *tag, const char *format, ...) // NOLINT + __attribute__((format(printf, 3, 4))); +#ifdef USE_STORE_LOG_STR_IN_FLASH +int esp_log_printf_(int level, const char *tag, const __FlashStringHelper *format, ...); +#endif +int esp_log_vprintf_(int level, const char *tag, const char *format, va_list args); // NOLINT +#ifdef USE_STORE_LOG_STR_IN_FLASH +int esp_log_vprintf_(int level, const char *tag, const __FlashStringHelper *format, va_list args); +#endif +int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT + +#ifdef USE_STORE_LOG_STR_IN_FLASH +#define ESPHOME_LOG_FORMAT(tag, letter, format) \ + F(ESPHOME_LOG_COLOR_##letter "[" #letter "][%s:%03u]: " format ESPHOME_LOG_RESET_COLOR), tag, __LINE__ +#else +#define ESPHOME_LOG_FORMAT(tag, letter, format) \ + ESPHOME_LOG_COLOR_##letter "[" #letter "][%s:%03u]: " format ESPHOME_LOG_RESET_COLOR, tag, __LINE__ +#endif + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE +#define esph_log_vv(tag, format, ...) \ + esp_log_printf_(ESPHOME_LOG_LEVEL_VERY_VERBOSE, tag, ESPHOME_LOG_FORMAT(tag, VV, format), ##__VA_ARGS__) + +#define ESPHOME_LOG_HAS_VERY_VERBOSE +#else +#define esph_log_vv(tag, format, ...) +#endif + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE +#define esph_log_v(tag, format, ...) \ + esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, ESPHOME_LOG_FORMAT(tag, V, format), ##__VA_ARGS__) + +#define ESPHOME_LOG_HAS_VERBOSE +#else +#define esph_log_v(tag, format, ...) +#endif + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG +#define esph_log_d(tag, format, ...) \ + esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, ESPHOME_LOG_FORMAT(tag, D, format), ##__VA_ARGS__) + +#define esph_log_config(tag, format, ...) \ + esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, ESPHOME_LOG_FORMAT(tag, C, format), ##__VA_ARGS__) + +#define ESPHOME_LOG_HAS_DEBUG +#define ESPHOME_LOG_HAS_CONFIG +#else +#define esph_log_d(tag, format, ...) + +#define esph_log_config(tag, format, ...) +#endif + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO +#define esph_log_i(tag, format, ...) \ + esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, ESPHOME_LOG_FORMAT(tag, I, format), ##__VA_ARGS__) + +#define ESPHOME_LOG_HAS_INFO +#else +#define esph_log_i(tag, format, ...) +#endif + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN +#define esph_log_w(tag, format, ...) \ + esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, ESPHOME_LOG_FORMAT(tag, W, format), ##__VA_ARGS__) + +#define ESPHOME_LOG_HAS_WARN +#else +#define esph_log_w(tag, format, ...) +#endif + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR +#define esph_log_e(tag, format, ...) \ + esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, ESPHOME_LOG_FORMAT(tag, E, format), ##__VA_ARGS__) + +#define ESPHOME_LOG_HAS_ERROR +#else +#define esph_log_e(tag, format, ...) +#endif + +#ifdef ESP_LOGE +#undef ESP_LOGE +#endif +#ifdef ESP_LOGW +#undef ESP_LOGW +#endif +#ifdef ESP_LOGI +#undef ESP_LOGI +#endif +#ifdef ESP_LOGD +#undef ESP_LOGD +#endif +#ifdef ESP_LOGV +#undef ESP_LOGV +#endif + +#define ESP_LOGE(tag, ...) esph_log_e(tag, __VA_ARGS__) +#define LOG_E(tag, ...) ESP_LOGE(tag, __VA__ARGS__) +#define ESP_LOGW(tag, ...) esph_log_w(tag, __VA_ARGS__) +#define LOG_W(tag, ...) ESP_LOGW(tag, __VA__ARGS__) +#define ESP_LOGI(tag, ...) esph_log_i(tag, __VA_ARGS__) +#define LOG_I(tag, ...) ESP_LOGI(tag, __VA__ARGS__) +#define ESP_LOGD(tag, ...) esph_log_d(tag, __VA_ARGS__) +#define LOG_D(tag, ...) ESP_LOGD(tag, __VA__ARGS__) +#define ESP_LOGCONFIG(tag, ...) esph_log_config(tag, __VA_ARGS__) +#define LOG_CONFIG(tag, ...) ESP_LOGCONFIG(tag, __VA__ARGS__) +#define ESP_LOGV(tag, ...) esph_log_v(tag, __VA_ARGS__) +#define LOG_V(tag, ...) ESP_LOGV(tag, __VA__ARGS__) +#define ESP_LOGVV(tag, ...) esph_log_vv(tag, __VA_ARGS__) +#define LOG_VV(tag, ...) ESP_LOGVV(tag, __VA__ARGS__) + +#define BYTE_TO_BINARY_PATTERN "%c%c%c%c%c%c%c%c" +#define BYTE_TO_BINARY(byte) \ + ((byte) &0x80 ? '1' : '0'), ((byte) &0x40 ? '1' : '0'), ((byte) &0x20 ? '1' : '0'), ((byte) &0x10 ? '1' : '0'), \ + ((byte) &0x08 ? '1' : '0'), ((byte) &0x04 ? '1' : '0'), ((byte) &0x02 ? '1' : '0'), ((byte) &0x01 ? '1' : '0') +#define YESNO(b) ((b) ? "YES" : "NO") +#define ONOFF(b) ((b) ? "ON" : "OFF") + +} // namespace esphome diff --git a/esphome/core/optional.h b/esphome/core/optional.h new file mode 100644 index 000000000..f6b050f5f --- /dev/null +++ b/esphome/core/optional.h @@ -0,0 +1,214 @@ +#pragma once +// +// Copyright (c) 2017 Martin Moene +// +// https://github.com/martinmoene/optional-bare +// +// This code is licensed under the MIT License (MIT). +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +// Modified by Otto Winter on 18.05.18 + +namespace esphome { + +// type for nullopt + +struct nullopt_t { // NOLINT + struct init {}; // NOLINT + nullopt_t(init) {} +}; + +// extra parenthesis to prevent the most vexing parse: + +const nullopt_t nullopt((nullopt_t::init())); // NOLINT + +// Simplistic optional: requires T to be default constructible, copyable. + +template class optional { // NOLINT + private: + using safe_bool = void (optional::*)() const; + + public: + using value_type = T; + + optional() {} + + optional(nullopt_t) {} + + optional(T const &arg) : has_value_(true), value_(arg) {} + + template optional(optional const &other) : has_value_(other.has_value()), value_(other.value()) {} + + optional &operator=(nullopt_t) { + reset(); + return *this; + } + + template optional &operator=(optional const &other) { + has_value_ = other.has_value(); + value_ = other.value(); + return *this; + } + + void swap(optional &rhs) { + using std::swap; + if (has_value() && rhs.has_value()) { + swap(**this, *rhs); + } else if (!has_value() && rhs.has_value()) { + initialize(*rhs); + rhs.reset(); + } else if (has_value() && !rhs.has_value()) { + rhs.initialize(**this); + reset(); + } + } + + // observers + + value_type const *operator->() const { return &value_; } + + value_type *operator->() { return &value_; } + + value_type const &operator*() const { return value_; } + + value_type &operator*() { return value_; } + + operator safe_bool() const { return has_value() ? &optional::this_type_does_not_support_comparisons : nullptr; } + + bool has_value() const { return has_value_; } + + value_type const &value() const { return value_; } + + value_type &value() { return value_; } + + template value_type value_or(U const &v) const { return has_value() ? value() : static_cast(v); } + + // modifiers + + void reset() { has_value_ = false; } + + private: + void this_type_does_not_support_comparisons() const {} // NOLINT + + template void initialize(V const &value) { // NOLINT + value_ = value; + has_value_ = true; + } + + private: + bool has_value_{false}; // NOLINT + value_type value_; // NOLINT +}; + +// Relational operators + +template inline bool operator==(optional const &x, optional const &y) { + return bool(x) != bool(y) ? false : !bool(x) ? true : *x == *y; +} + +template inline bool operator!=(optional const &x, optional const &y) { + return !(x == y); +} + +template inline bool operator<(optional const &x, optional const &y) { + return (!y) ? false : (!x) ? true : *x < *y; +} + +template inline bool operator>(optional const &x, optional const &y) { return (y < x); } + +template inline bool operator<=(optional const &x, optional const &y) { return !(y < x); } + +template inline bool operator>=(optional const &x, optional const &y) { return !(x < y); } + +// Comparison with nullopt + +template inline bool operator==(optional const &x, nullopt_t) { return (!x); } + +template inline bool operator==(nullopt_t, optional const &x) { return (!x); } + +template inline bool operator!=(optional const &x, nullopt_t) { return bool(x); } + +template inline bool operator!=(nullopt_t, optional const &x) { return bool(x); } + +template inline bool operator<(optional const &, nullopt_t) { return false; } + +template inline bool operator<(nullopt_t, optional const &x) { return bool(x); } + +template inline bool operator<=(optional const &x, nullopt_t) { return (!x); } + +template inline bool operator<=(nullopt_t, optional const &) { return true; } + +template inline bool operator>(optional const &x, nullopt_t) { return bool(x); } + +template inline bool operator>(nullopt_t, optional const &) { return false; } + +template inline bool operator>=(optional const &, nullopt_t) { return true; } + +template inline bool operator>=(nullopt_t, optional const &x) { return (!x); } + +// Comparison with T + +template inline bool operator==(optional const &x, U const &v) { + return bool(x) ? *x == v : false; +} + +template inline bool operator==(U const &v, optional const &x) { + return bool(x) ? v == *x : false; +} + +template inline bool operator!=(optional const &x, U const &v) { + return bool(x) ? *x != v : true; +} + +template inline bool operator!=(U const &v, optional const &x) { + return bool(x) ? v != *x : true; +} + +template inline bool operator<(optional const &x, U const &v) { + return bool(x) ? *x < v : true; +} + +template inline bool operator<(U const &v, optional const &x) { + return bool(x) ? v < *x : false; +} + +template inline bool operator<=(optional const &x, U const &v) { + return bool(x) ? *x <= v : true; +} + +template inline bool operator<=(U const &v, optional const &x) { + return bool(x) ? v <= *x : false; +} + +template inline bool operator>(optional const &x, U const &v) { + return bool(x) ? *x > v : false; +} + +template inline bool operator>(U const &v, optional const &x) { + return bool(x) ? v > *x : true; +} + +template inline bool operator>=(optional const &x, U const &v) { + return bool(x) ? *x >= v : false; +} + +template inline bool operator>=(U const &v, optional const &x) { + return bool(x) ? v >= *x : true; +} + +// Specialized algorithms + +template void swap(optional &x, optional &y) { x.swap(y); } + +// Convenience function to create an optional. + +template inline optional make_optional(T const &v) { return optional(v); } + +} // namespace esphome diff --git a/esphome/core/preferences.cpp b/esphome/core/preferences.cpp new file mode 100644 index 000000000..1847b1a03 --- /dev/null +++ b/esphome/core/preferences.cpp @@ -0,0 +1,233 @@ +#include "esphome/core/preferences.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#ifdef USE_ESP8266_PREFERENCES_FLASH +extern "C" { +#include "spi_flash.h" +} +#endif + +namespace esphome { + +static const char *TAG = "preferences"; + +ESPPreferenceObject::ESPPreferenceObject() : rtc_offset_(0), length_words_(0), type_(0), data_(nullptr) {} +ESPPreferenceObject::ESPPreferenceObject(size_t rtc_offset, size_t length, uint32_t type) + : rtc_offset_(rtc_offset), length_words_(length), type_(type) { + this->data_ = new uint32_t[this->length_words_ + 1]; + for (uint32_t i = 0; i < this->length_words_ + 1; i++) + this->data_[i] = 0; +} +bool ESPPreferenceObject::load_() { + if (!this->is_initialized()) { + ESP_LOGV(TAG, "Load Pref Not initialized!"); + return false; + } + if (!this->load_internal_()) + return false; + + bool valid = this->data_[this->length_words_] == this->calculate_crc_(); + + ESP_LOGVV(TAG, "LOAD %zu: valid=%s, 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->rtc_offset_, // NOLINT + YESNO(valid), this->data_[0], this->data_[1], this->type_, this->calculate_crc_()); + return valid; +} +bool ESPPreferenceObject::save_() { + if (!this->is_initialized()) { + ESP_LOGV(TAG, "Save Pref Not initialized!"); + return false; + } + + this->data_[this->length_words_] = this->calculate_crc_(); + if (!this->save_internal_()) + return false; + ESP_LOGVV(TAG, "SAVE %zu: 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->rtc_offset_, // NOLINT + this->data_[0], this->data_[1], this->type_, this->calculate_crc_()); + return true; +} + +#ifdef ARDUINO_ARCH_ESP8266 + +#define ESP_RTC_USER_MEM_START 0x60001200 +#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START) +#define ESP_RTC_USER_MEM_SIZE_WORDS 128 +#define ESP_RTC_USER_MEM_SIZE_BYTES ESP_RTC_USER_MEM_SIZE_WORDS * 4 + +static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) { + if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) { + return false; + } + *dest = ESP_RTC_USER_MEM[index]; + return true; +} + +#ifdef USE_ESP8266_PREFERENCES_FLASH +static bool esp8266_preferences_modified = false; +#endif + +static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) { + if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) { + return false; + } + if (index < 32 && global_preferences.is_prevent_write()) { + return false; + } + + auto *ptr = &ESP_RTC_USER_MEM[index]; +#ifdef USE_ESP8266_PREFERENCES_FLASH + if (*ptr != value) { + esp8266_preferences_modified = true; + } +#endif + *ptr = value; + return true; +} + +#ifdef USE_ESP8266_PREFERENCES_FLASH +extern "C" uint32_t _SPIFFS_end; + +static const uint32_t get_esp8266_flash_sector() { return (uint32_t(&_SPIFFS_end) - 0x40200000) / SPI_FLASH_SEC_SIZE; } +static const uint32_t get_esp8266_flash_address() { return get_esp8266_flash_sector() * SPI_FLASH_SEC_SIZE; } + +static void load_esp8266_flash() { + ESP_LOGVV(TAG, "Loading preferences from flash..."); + disable_interrupts(); + spi_flash_read(get_esp8266_flash_address(), ESP_RTC_USER_MEM, ESP_RTC_USER_MEM_SIZE_BYTES); + enable_interrupts(); +} +static void save_esp8266_flash() { + if (!esp8266_preferences_modified) + return; + + ESP_LOGVV(TAG, "Saving preferences to flash..."); + disable_interrupts(); + auto erase_res = spi_flash_erase_sector(get_esp8266_flash_sector()); + if (erase_res != SPI_FLASH_RESULT_OK) { + enable_interrupts(); + ESP_LOGV(TAG, "Erase ESP8266 flash failed!"); + return; + } + + auto write_res = spi_flash_write(get_esp8266_flash_address(), ESP_RTC_USER_MEM, ESP_RTC_USER_MEM_SIZE_BYTES); + enable_interrupts(); + if (write_res != SPI_FLASH_RESULT_OK) { + ESP_LOGV(TAG, "Write ESP8266 flash failed!"); + return; + } + + esp8266_preferences_modified = false; +} +#endif + +bool ESPPreferenceObject::save_internal_() { + for (uint32_t i = 0; i <= this->length_words_; i++) { + if (!esp_rtc_user_mem_write(this->rtc_offset_ + i, this->data_[i])) + return false; + } + +#ifdef USE_ESP8266_PREFERENCES_FLASH + save_esp8266_flash(); +#endif + return true; +} +bool ESPPreferenceObject::load_internal_() { + for (uint32_t i = 0; i <= this->length_words_; i++) { + if (!esp_rtc_user_mem_read(this->rtc_offset_ + i, &this->data_[i])) + return false; + } + return true; +} +ESPPreferences::ESPPreferences() + // offset starts from start of user RTC mem (64 words before that are reserved for system), + // an additional 32 words at the start of user RTC are for eboot (OTA, see eboot_command.h), + // which will be reset each time OTA occurs + : current_offset_(0) {} + +void ESPPreferences::begin(const std::string &name) { +#ifdef USE_ESP8266_PREFERENCES_FLASH + load_esp8266_flash(); +#endif +} + +ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type) { + uint32_t start = this->current_offset_; + uint32_t end = start + length + 1; + bool in_normal = start < 96; + // Normal: offset 0-95 maps to RTC offset 32 - 127, + // Eboot: offset 96-127 maps to RTC offset 0 - 31 words + if (in_normal && end > 96) { + // start is in normal but end is not -> switch to Eboot + this->current_offset_ = start = 96; + end = start + length + 1; + in_normal = false; + } + + if (end > 128) { + // Doesn't fit in data, return uninitialized preference obj. + return ESPPreferenceObject(); + } + + uint32_t rtc_offset; + if (in_normal) { + rtc_offset = start + 32; + } else { + rtc_offset = start - 96; + } + + auto pref = ESPPreferenceObject(rtc_offset, length, type); + this->current_offset_ += length + 1; + return pref; +} +void ESPPreferences::prevent_write(bool prevent) { this->prevent_write_ = prevent; } +bool ESPPreferences::is_prevent_write() { return this->prevent_write_; } +#endif + +#ifdef ARDUINO_ARCH_ESP32 +bool ESPPreferenceObject::save_internal_() { + char key[32]; + sprintf(key, "%u", this->rtc_offset_); + uint32_t len = (this->length_words_ + 1) * 4; + size_t ret = global_preferences.preferences_.putBytes(key, this->data_, len); + if (ret != len) { + ESP_LOGV(TAG, "putBytes failed!"); + return false; + } + return true; +} +bool ESPPreferenceObject::load_internal_() { + char key[32]; + sprintf(key, "%u", this->rtc_offset_); + uint32_t len = (this->length_words_ + 1) * 4; + size_t ret = global_preferences.preferences_.getBytes(key, this->data_, len); + if (ret != len) { + ESP_LOGV(TAG, "getBytes failed!"); + return false; + } + return true; +} +ESPPreferences::ESPPreferences() : current_offset_(0) {} +void ESPPreferences::begin(const std::string &name) { + const std::string key = truncate_string(name, 15); + ESP_LOGV(TAG, "Opening preferences with key '%s'", key.c_str()); + this->preferences_.begin(key.c_str()); +} + +ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type) { + auto pref = ESPPreferenceObject(this->current_offset_, length, type); + this->current_offset_++; + return pref; +} +#endif +uint32_t ESPPreferenceObject::calculate_crc_() const { + uint32_t crc = this->type_; + for (size_t i = 0; i < this->length_words_; i++) { + crc ^= (this->data_[i] * 2654435769UL) >> 1; + } + return crc; +} +bool ESPPreferenceObject::is_initialized() const { return this->data_ != nullptr; } + +ESPPreferences global_preferences; + +} // namespace esphome diff --git a/esphome/core/preferences.h b/esphome/core/preferences.h new file mode 100644 index 000000000..32f525649 --- /dev/null +++ b/esphome/core/preferences.h @@ -0,0 +1,92 @@ +#pragma once + +#include + +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif + +#include "esphome/core/esphal.h" + +namespace esphome { + +class ESPPreferenceObject { + public: + ESPPreferenceObject(); + ESPPreferenceObject(size_t rtc_offset, size_t length, uint32_t type); + + template bool save(T *src); + + template bool load(T *dest); + + bool is_initialized() const; + + protected: + bool save_(); + bool load_(); + bool save_internal_(); + bool load_internal_(); + + uint32_t calculate_crc_() const; + + size_t rtc_offset_; + size_t length_words_; + uint32_t type_; + uint32_t *data_; +}; + +class ESPPreferences { + public: + ESPPreferences(); + void begin(const std::string &name); + ESPPreferenceObject make_preference(size_t length, uint32_t type); + template ESPPreferenceObject make_preference(uint32_t type); + +#ifdef ARDUINO_ARCH_ESP8266 + /** On the ESP8266, we can't override the first 128 bytes during OTA uploads + * as the eboot parameters are stored there. Writing there during an OTA upload + * would invalidate applying the new firmware. During normal operation, we use + * this part of the RTC user memory, but stop writing to it during OTA uploads. + * + * @param prevent Whether to prevent writing to the first 32 words of RTC user memory. + */ + void prevent_write(bool prevent); + bool is_prevent_write(); +#endif + + protected: + friend ESPPreferenceObject; + + uint32_t current_offset_; +#ifdef ARDUINO_ARCH_ESP32 + Preferences preferences_; +#endif +#ifdef ARDUINO_ARCH_ESP8266 + bool prevent_write_{false}; +#endif +}; + +extern ESPPreferences global_preferences; + +template ESPPreferenceObject ESPPreferences::make_preference(uint32_t type) { + return this->make_preference((sizeof(T) + 3) / 4, type); +} + +template bool ESPPreferenceObject::save(T *src) { + if (!this->is_initialized()) + return false; + memset(this->data_, 0, this->length_words_ * 4); + memcpy(this->data_, src, sizeof(T)); + return this->save_(); +} + +template bool ESPPreferenceObject::load(T *dest) { + memset(this->data_, 0, this->length_words_ * 4); + if (!this->load_()) + return false; + + memcpy(dest, this->data_, sizeof(T)); + return true; +} + +} // namespace esphome diff --git a/esphome/core/util.cpp b/esphome/core/util.cpp new file mode 100644 index 000000000..9e70f8b6e --- /dev/null +++ b/esphome/core/util.cpp @@ -0,0 +1,120 @@ +#include "esphome/core/util.h" +#include "esphome/core/defines.h" +#include "esphome/core/application.h" + +#ifdef USE_WIFI +#include "esphome/components/wifi/wifi_component.h" +#endif + +#ifdef USE_API +#include "esphome/components/api/api_server.h" +#endif + +#ifdef USE_ETHERNET +#include "esphome/components/ethernet/ethernet_component.h" +#endif + +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif +#ifdef ARDUINO_ARCH_ESP8266 +#include +#endif + +namespace esphome { + +bool network_is_connected() { +#ifdef USE_ETHERNET + if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected()) + return true; +#endif + +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) + return wifi::global_wifi_component->is_connected(); +#endif + + return false; +} + +void network_setup() { + bool ready = true; +#ifdef USE_ETHERNET + if (ethernet::global_eth_component != nullptr) { + ethernet::global_eth_component->call_setup(); + ready = false; + } +#endif + +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) { + wifi::global_wifi_component->call_setup(); + ready = false; + } +#endif + + while (!ready) { +#ifdef USE_ETHERNET + if (ethernet::global_eth_component != nullptr) { + ethernet::global_eth_component->call_loop(); + ready = ready || ethernet::global_eth_component->can_proceed(); + } +#endif +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) { + wifi::global_wifi_component->call_loop(); + ready = ready || wifi::global_wifi_component->can_proceed(); + } +#endif + + App.feed_wdt(); + } +} +void network_tick() { +#ifdef USE_ETHERNET + if (ethernet::global_eth_component != nullptr) + ethernet::global_eth_component->call_loop(); +#endif +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) + wifi::global_wifi_component->call_loop(); +#endif +} + +void network_setup_mdns() { + MDNS.begin(App.get_name().c_str()); +#ifdef USE_API + if (api::global_api_server != nullptr) { + MDNS.addService("esphomelib", "tcp", api::global_api_server->get_port()); + // DNS-SD (!=mDNS !) requires at least one TXT record for service discovery - let's add version + MDNS.addServiceTxt("esphomelib", "tcp", "version", ESPHOME_VERSION); + MDNS.addServiceTxt("esphomelib", "tcp", "address", network_get_address().c_str()); + } else { +#endif + // Publish "http" service if not using native API. + // This is just to have *some* mDNS service so that .local resolution works + MDNS.addService("http", "tcp", 80); + MDNS.addServiceTxt("http", "tcp", "version", ESPHOME_VERSION); +#ifdef USE_API + } +#endif +} +void network_tick_mdns() { +#ifdef ARDUINO_ARCH_ESP8266 + MDNS.update(); +#endif +} + +std::string network_get_address() { +#ifdef USE_ETHERNET + if (ethernet::global_eth_component != nullptr) + return ethernet::global_eth_component->get_use_address(); +#endif +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) + return wifi::global_wifi_component->get_use_address(); +#endif + return ""; +} + +} // namespace esphome diff --git a/esphome/core/util.h b/esphome/core/util.h new file mode 100644 index 000000000..f47eeb843 --- /dev/null +++ b/esphome/core/util.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +namespace esphome { + +/// Return whether the node is connected to the network (through wifi, eth, ...) +bool network_is_connected(); +/// Get the active network hostname +std::string network_get_address(); + +/// Manually set up the network stack (outside of the App.setup() loop, for example in OTA safe mode) +void network_setup(); +void network_tick(); +void network_setup_mdns(); +void network_tick_mdns(); + +} // namespace esphome diff --git a/esphome/core_config.py b/esphome/core_config.py index 86cc66f3c..a9b394277 100644 --- a/esphome/core_config.py +++ b/esphome/core_config.py @@ -2,31 +2,26 @@ import logging import os import re -import voluptuous as vol - -from esphome import automation, pins +import esphome.codegen as cg import esphome.config_validation as cv +from esphome import automation, pins from esphome.const import ARDUINO_VERSION_ESP32_DEV, ARDUINO_VERSION_ESP8266_DEV, \ - CONF_ARDUINO_VERSION, CONF_BOARD, CONF_BOARD_FLASH_MODE, CONF_BRANCH, CONF_BUILD_PATH, \ - CONF_COMMIT, CONF_ESPHOME, CONF_ESPHOME_CORE_VERSION, CONF_INCLUDES, CONF_LIBRARIES, \ - CONF_LOCAL, CONF_NAME, CONF_ON_BOOT, CONF_ON_LOOP, CONF_ON_SHUTDOWN, CONF_PLATFORM, \ - CONF_PLATFORMIO_OPTIONS, CONF_PRIORITY, CONF_REPOSITORY, CONF_TAG, CONF_TRIGGER_ID, \ - CONF_USE_CUSTOM_CODE, ESPHOME_CORE_VERSION, ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266, \ - CONF_ESP8266_RESTORE_FROM_FLASH -from esphome.core import CORE, EsphomeError -from esphome.cpp_generator import Pvariable, RawExpression, add -from esphome.cpp_types import App, const_char_ptr, esphome_ns + CONF_ARDUINO_VERSION, CONF_BOARD, CONF_BOARD_FLASH_MODE, CONF_BUILD_PATH, \ + CONF_ESPHOME, CONF_INCLUDES, CONF_LIBRARIES, \ + CONF_NAME, CONF_ON_BOOT, CONF_ON_LOOP, CONF_ON_SHUTDOWN, CONF_PLATFORM, \ + CONF_PLATFORMIO_OPTIONS, CONF_PRIORITY, CONF_TRIGGER_ID, \ + CONF_ESP8266_RESTORE_FROM_FLASH, __version__, ARDUINO_VERSION_ESP8266_2_3_0, \ + ARDUINO_VERSION_ESP8266_2_5_0 +from esphome.core import CORE, EsphomeError, coroutine_with_priority +from esphome.pins import ESP8266_FLASH_SIZES, ESP8266_LD_SCRIPTS from esphome.py_compat import text_type _LOGGER = logging.getLogger(__name__) -LIBRARY_URI_REPO = u'https://github.com/esphome/esphome-core.git' -GITHUB_ARCHIVE_ZIP = u'https://github.com/esphome/esphome-core/archive/{}.zip' - BUILD_FLASH_MODES = ['qio', 'qout', 'dio', 'dout'] -StartupTrigger = esphome_ns.StartupTrigger -ShutdownTrigger = esphome_ns.ShutdownTrigger -LoopTrigger = esphome_ns.LoopTrigger +StartupTrigger = cg.esphome_ns.StartupTrigger +ShutdownTrigger = cg.esphome_ns.ShutdownTrigger +LoopTrigger = cg.esphome_ns.LoopTrigger VERSION_REGEX = re.compile(r'^[0-9]+\.[0-9]+\.[0-9]+(?:[ab]\d+)?$') @@ -40,76 +35,12 @@ def validate_board(value): raise NotImplementedError if value not in board_pins: - raise vol.Invalid(u"Could not find board '{}'. Valid boards are {}".format( + raise cv.Invalid(u"Could not find board '{}'. Valid boards are {}".format( value, u', '.join(pins.ESP8266_BOARD_PINS.keys()))) return value -def validate_simple_esphome_core_version(value): - value = cv.string_strict(value) - if value.upper() == 'LATEST': - if ESPHOME_CORE_VERSION == 'dev': - return validate_simple_esphome_core_version('dev') - return { - CONF_REPOSITORY: LIBRARY_URI_REPO, - CONF_TAG: 'v' + ESPHOME_CORE_VERSION, - } - if value.upper() == 'DEV': - return { - CONF_REPOSITORY: LIBRARY_URI_REPO, - CONF_BRANCH: 'dev' - } - if VERSION_REGEX.match(value) is not None: - return { - CONF_REPOSITORY: LIBRARY_URI_REPO, - CONF_TAG: 'v' + value, - } - raise vol.Invalid("Only simple esphome core versions!") - - -def validate_local_esphome_core_version(value): - value = cv.directory(value) - path = CORE.relative_path(value) - library_json = os.path.join(path, 'library.json') - if not os.path.exists(library_json): - raise vol.Invalid(u"Could not find '{}' file. '{}' does not seem to point to an " - u"esphome-core copy.".format(library_json, value)) - return value - - -def validate_commit(value): - value = cv.string(value) - if re.match(r"^[0-9a-f]{7,}$", value) is None: - raise vol.Invalid("Commit option only accepts commit hashes in hex format.") - return value - - -ESPHOME_CORE_VERSION_SCHEMA = vol.Any( - validate_simple_esphome_core_version, - cv.Schema({ - vol.Required(CONF_LOCAL): validate_local_esphome_core_version, - }), - vol.All( - cv.Schema({ - vol.Optional(CONF_REPOSITORY, default=LIBRARY_URI_REPO): cv.string, - vol.Optional(CONF_COMMIT): validate_commit, - vol.Optional(CONF_BRANCH): cv.string, - vol.Optional(CONF_TAG): cv.string, - }), - cv.has_at_most_one_key(CONF_COMMIT, CONF_BRANCH, CONF_TAG) - ), -) - - -def validate_platform(value): - value = cv.string(value) - if value.upper() in ('ESP8266', 'ESPRESSIF8266'): - return ESP_PLATFORM_ESP8266 - if value.upper() in ('ESP32', 'ESPRESSIF32'): - return ESP_PLATFORM_ESP32 - raise vol.Invalid(u"Invalid platform '{}'. Only options are ESP8266 and ESP32. Please note " - u"the old way to use the latest arduino framework version has been split up " - u"into the arduino_version configuration option.".format(value)) +validate_platform = cv.one_of('ESP32', 'ESP8266', upper=True) PLATFORMIO_ESP8266_LUT = { @@ -137,17 +68,17 @@ def validate_arduino_version(value): value_ = value.upper() if CORE.is_esp8266: if VERSION_REGEX.match(value) is not None and value_ not in PLATFORMIO_ESP8266_LUT: - raise vol.Invalid("Unfortunately the arduino framework version '{}' is unsupported " - "at this time. You can override this by manually using " - "espressif8266@") + raise cv.Invalid("Unfortunately the arduino framework version '{}' is unsupported " + "at this time. You can override this by manually using " + "espressif8266@") if value_ in PLATFORMIO_ESP8266_LUT: return PLATFORMIO_ESP8266_LUT[value_] return value if CORE.is_esp32: if VERSION_REGEX.match(value) is not None and value_ not in PLATFORMIO_ESP32_LUT: - raise vol.Invalid("Unfortunately the arduino framework version '{}' is unsupported " - "at this time. You can override this by manually using " - "espressif32@") + raise cv.Invalid("Unfortunately the arduino framework version '{}' is unsupported " + "at this time. You can override this by manually using " + "espressif32@") if value_ in PLATFORMIO_ESP32_LUT: return PLATFORMIO_ESP32_LUT[value_] return value @@ -159,35 +90,32 @@ def default_build_path(): CONFIG_SCHEMA = cv.Schema({ - vol.Required(CONF_NAME): cv.valid_name, - vol.Required(CONF_PLATFORM): cv.one_of('ESP8266', 'ESPRESSIF8266', 'ESP32', 'ESPRESSIF32', - upper=True), - vol.Required(CONF_BOARD): validate_board, - vol.Optional(CONF_ESPHOME_CORE_VERSION, default='latest'): ESPHOME_CORE_VERSION_SCHEMA, - vol.Optional(CONF_ARDUINO_VERSION, default='recommended'): validate_arduino_version, - vol.Optional(CONF_USE_CUSTOM_CODE, default=False): cv.boolean, - vol.Optional(CONF_BUILD_PATH, default=default_build_path): cv.string, - vol.Optional(CONF_PLATFORMIO_OPTIONS): cv.Schema({ - cv.string_strict: vol.Any([cv.string], cv.string), + cv.Required(CONF_NAME): cv.valid_name, + cv.Required(CONF_PLATFORM): cv.one_of('ESP8266', 'ESP32', 'ESPRESSIF32', + upper=True), + cv.Required(CONF_BOARD): validate_board, + cv.Optional(CONF_ARDUINO_VERSION, default='recommended'): validate_arduino_version, + cv.Optional(CONF_BUILD_PATH, default=default_build_path): cv.string, + cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema({ + cv.string_strict: cv.Any([cv.string], cv.string), }), - vol.Optional(CONF_ESP8266_RESTORE_FROM_FLASH): vol.All(cv.only_on_esp8266, cv.boolean), + cv.SplitDefault(CONF_ESP8266_RESTORE_FROM_FLASH, esp8266=False): cv.All(cv.only_on_esp8266, + cv.boolean), - vol.Optional(CONF_BOARD_FLASH_MODE, default='dout'): cv.one_of(*BUILD_FLASH_MODES, lower=True), - vol.Optional(CONF_ON_BOOT): automation.validate_automation({ + cv.SplitDefault(CONF_BOARD_FLASH_MODE, esp8266='dout'): cv.one_of(*BUILD_FLASH_MODES, + lower=True), + cv.Optional(CONF_ON_BOOT): automation.validate_automation({ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(StartupTrigger), - vol.Optional(CONF_PRIORITY): cv.float_, + cv.Optional(CONF_PRIORITY): cv.float_, }), - vol.Optional(CONF_ON_SHUTDOWN): automation.validate_automation({ + cv.Optional(CONF_ON_SHUTDOWN): automation.validate_automation({ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(ShutdownTrigger), }), - vol.Optional(CONF_ON_LOOP): automation.validate_automation({ + cv.Optional(CONF_ON_LOOP): automation.validate_automation({ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(LoopTrigger), }), - vol.Optional(CONF_INCLUDES): cv.ensure_list(cv.file_), - vol.Optional(CONF_LIBRARIES): cv.ensure_list(cv.string_strict), - - vol.Optional('esphomelib_version'): cv.invalid("The esphomelib_version has been renamed to " - "esphome_core_version in 1.11.0"), + cv.Optional(CONF_INCLUDES, default=[]): cv.ensure_list(cv.file_), + cv.Optional(CONF_LIBRARIES, default=[]): cv.ensure_list(cv.string_strict), }) @@ -212,44 +140,80 @@ def preload_core_config(config): CORE.name = cv.valid_name(core_conf[CONF_NAME]) CORE.build_path = CORE.relative_path( cv.string(core_conf.get(CONF_BUILD_PATH, default_build_path()))) - except vol.Invalid as e: + except cv.Invalid as e: raise EsphomeError(text_type(e)) -def to_code(config): - add(App.set_name(config[CONF_NAME])) - - for conf in config.get(CONF_ON_BOOT, []): - rhs = App.register_component(StartupTrigger.new(conf.get(CONF_PRIORITY))) - trigger = Pvariable(conf[CONF_TRIGGER_ID], rhs) - automation.build_automations(trigger, [], conf) - - for conf in config.get(CONF_ON_SHUTDOWN, []): - trigger = Pvariable(conf[CONF_TRIGGER_ID], ShutdownTrigger.new()) - automation.build_automations(trigger, [(const_char_ptr, 'x')], conf) - - for conf in config.get(CONF_ON_LOOP, []): - rhs = App.register_component(LoopTrigger.new()) - trigger = Pvariable(conf[CONF_TRIGGER_ID], rhs) - automation.build_automations(trigger, [], conf) - - add(App.set_compilation_datetime(RawExpression('__DATE__ ", " __TIME__'))) - - -def lib_deps(config): - return set(config.get(CONF_LIBRARIES, [])) - - -def includes(config): - ret = [] - for include in config.get(CONF_INCLUDES, []): +@coroutine_with_priority(-1000.0) +def add_includes(includes): + # Add includes at the very end, so that the included files can access global variables + for include in includes: path = CORE.relative_path(include) res = os.path.relpath(path, CORE.relative_build_path('src')) - ret.append(u'#include "{}"'.format(res)) - return ret + cg.add_global(cg.RawExpression(u'#include "{}"'.format(res))) -def required_build_flags(config): +@coroutine_with_priority(100.0) +def to_code(config): + cg.add_global(cg.global_ns.namespace('esphome').using) + cg.add_define('ESPHOME_VERSION', __version__) + cg.add(cg.App.pre_setup(config[CONF_NAME], cg.RawExpression('__DATE__ ", " __TIME__'))) + + for conf in config.get(CONF_ON_BOOT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf.get(CONF_PRIORITY)) + yield cg.register_component(trigger, conf) + yield automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_SHUTDOWN, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + yield cg.register_component(trigger, conf) + yield automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_LOOP, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + yield cg.register_component(trigger, conf) + yield automation.build_automation(trigger, [], conf) + + # Build flags + if CORE.is_esp8266 and CORE.board in ESP8266_FLASH_SIZES and \ + CORE.arduino_version != ARDUINO_VERSION_ESP8266_2_3_0: + flash_size = ESP8266_FLASH_SIZES[CORE.board] + ld_scripts = ESP8266_LD_SCRIPTS[flash_size] + ld_script = None + + if CORE.arduino_version in ('espressif8266@1.8.0', 'espressif8266@1.7.3', + 'espressif8266@1.6.0'): + ld_script = ld_scripts[0] + elif CORE.arduino_version in (ARDUINO_VERSION_ESP8266_DEV, ARDUINO_VERSION_ESP8266_2_5_0): + ld_script = ld_scripts[1] + + if ld_script is not None: + cg.add_build_flag('-Wl,-T{}'.format(ld_script)) + + if CORE.is_esp8266 and CORE.arduino_version in (ARDUINO_VERSION_ESP8266_DEV, + ARDUINO_VERSION_ESP8266_2_5_0): + cg.add_build_flag('-fno-exceptions') + + # Libraries + if CORE.is_esp32: + cg.add_library('Preferences', None) + cg.add_library('ESPmDNS', None) + elif CORE.is_esp8266: + cg.add_library('ESP8266WiFi', None) + cg.add_library('ESP8266mDNS', None) + + for lib in config[CONF_LIBRARIES]: + if '@' in lib: + name, vers = lib.split('@', 1) + cg.add_library(name, vers) + else: + cg.add_library(lib, None) + + cg.add_build_flag('-Wno-unused-variable') + cg.add_build_flag('-Wno-unused-but-set-variable') + cg.add_build_flag('-Wno-sign-compare') if config.get(CONF_ESP8266_RESTORE_FROM_FLASH, False): - return ['-DUSE_ESP8266_PREFERENCES_FLASH'] - return [] + cg.add_define('USE_ESP8266_PREFERENCES_FLASH') + + if config[CONF_INCLUDES]: + CORE.add_job(add_includes, config[CONF_INCLUDES]) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index efe8d1a4c..9334545a5 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -1,4 +1,3 @@ -from collections import OrderedDict import math # pylint: disable=unused-import, wrong-import-order @@ -6,32 +5,19 @@ from typing import Any, Generator, List, Optional, Tuple, Type, Union, Dict, Cal from esphome.core import ( # noqa CORE, HexInt, ID, Lambda, TimePeriod, TimePeriodMicroseconds, - TimePeriodMilliseconds, TimePeriodMinutes, TimePeriodSeconds, coroutine) + TimePeriodMilliseconds, TimePeriodMinutes, TimePeriodSeconds, coroutine, Library, Define) from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last from esphome.py_compat import integer_types, string_types, text_type +from esphome.util import OrderedDict class Expression(object): - def __init__(self): - self.requires = [] - self.required = False - def __str__(self): raise NotImplementedError - def require(self): - self.required = True - for require in self.requires: - if require.required: - continue - require.require() - - def has_side_effects(self): - return self.required - SafeExpType = Union[Expression, bool, str, text_type, int, float, TimePeriod, - Type[bool], Type[int], Type[float]] + Type[bool], Type[int], Type[float], List[Any]] class RawExpression(Expression): @@ -51,15 +37,23 @@ class AssignmentExpression(Expression): self.modifier = modifier self.name = name self.rhs = safe_exp(rhs) - self.requires.append(self.rhs) self.obj = obj def __str__(self): - type_ = self.type - return u"{} {}{} = {}".format(type_, self.modifier, self.name, self.rhs) + if self.type is None: + return u"{} = {}".format(self.name, self.rhs) + return u"{} {}{} = {}".format(self.type, self.modifier, self.name, self.rhs) - def has_side_effects(self): - return self.rhs.has_side_effects() + +class VariableDeclarationExpression(Expression): + def __init__(self, type, modifier, name): + super(VariableDeclarationExpression, self).__init__() + self.type = type + self.modifier = modifier + self.name = name + + def __str__(self): + return u"{} {}{}".format(self.type, self.modifier, self.name) class ExpressionList(Expression): @@ -69,11 +63,7 @@ class ExpressionList(Expression): args = list(args) while args and args[-1] is None: args.pop() - self.args = [] - for arg in args: - exp = safe_exp(arg) - self.requires.append(exp) - self.args.append(exp) + self.args = [safe_exp(arg) for arg in args] def __str__(self): text = u", ".join(text_type(x) for x in self.args) @@ -84,7 +74,6 @@ class TemplateArguments(Expression): def __init__(self, *args): # type: (*SafeExpType) -> None super(TemplateArguments, self).__init__() self.args = ExpressionList(*args) - self.requires.append(self.args) def __str__(self): return u'<{}>'.format(self.args) @@ -96,12 +85,10 @@ class CallExpression(Expression): self.base = base if args and isinstance(args[0], TemplateArguments): self.template_args = args[0] - self.requires.append(self.template_args) args = args[1:] else: self.template_args = None self.args = ExpressionList(*args) - self.requires.append(self.args) def __str__(self): if self.template_args is not None: @@ -113,8 +100,6 @@ class StructInitializer(Expression): def __init__(self, base, *args): # type: (Expression, *Tuple[str, SafeExpType]) -> None super(StructInitializer, self).__init__() self.base = base - if isinstance(base, Expression): - self.requires.append(base) if not isinstance(args, OrderedDict): args = OrderedDict(args) self.args = OrderedDict() @@ -123,7 +108,6 @@ class StructInitializer(Expression): continue exp = safe_exp(value) self.args[key] = exp - self.requires.append(exp) def __str__(self): cpp = u'{}{{\n'.format(self.base) @@ -143,7 +127,6 @@ class ArrayInitializer(Expression): continue exp = safe_exp(arg) self.args.append(exp) - self.requires.append(exp) def __str__(self): if not self.args: @@ -161,7 +144,7 @@ class ArrayInitializer(Expression): class ParameterExpression(Expression): def __init__(self, type, id): super(ParameterExpression, self).__init__() - self.type = type + self.type = safe_exp(type) self.id = id def __str__(self): @@ -176,7 +159,6 @@ class ParameterListExpression(Expression): if not isinstance(parameter, ParameterExpression): parameter = ParameterExpression(*parameter) self.parameters.append(parameter) - self.requires.append(parameter) def __str__(self): return u", ".join(text_type(x) for x in self.parameters) @@ -189,13 +171,8 @@ class LambdaExpression(Expression): if not isinstance(parameters, ParameterListExpression): parameters = ParameterListExpression(*parameters) self.parameters = parameters - self.requires.append(self.parameters) self.capture = capture self.return_type = safe_exp(return_type) if return_type is not None else None - if return_type is not None: - self.requires.append(self.return_type) - for i in range(1, len(parts), 3): - self.requires.append(parts[i]) def __str__(self): cpp = u'[{}]({})'.format(self.capture, self.parameters) @@ -348,7 +325,6 @@ def progmem_array(id, rhs): assignment = ProgmemAssignmentExpression(id.type, id, rhs, obj) CORE.add(assignment) CORE.register_variable(id, obj) - obj.requires.append(assignment) return obj @@ -365,38 +341,71 @@ def variable(id, # type: ID # type: (...) -> MockObj rhs = safe_exp(rhs) obj = MockObj(id, u'.') - id.type = type or id.type + if type is not None: + id.type = type assignment = AssignmentExpression(id.type, '', id, rhs, obj) CORE.add(assignment) CORE.register_variable(id, obj) - obj.requires.append(assignment) return obj def Pvariable(id, # type: ID rhs, # type: Expression - has_side_effects=True, # type: bool type=None # type: MockObj ): # type: (...) -> MockObj rhs = safe_exp(rhs) - if not has_side_effects and hasattr(rhs, '_has_side_effects'): - # pylint: disable=attribute-defined-outside-init, protected-access - rhs._has_side_effects = False - obj = MockObj(id, u'->', has_side_effects=has_side_effects) - id.type = type or id.type - assignment = AssignmentExpression(id.type, '*', id, rhs, obj) + obj = MockObj(id, u'->') + if type is not None: + id.type = type + decl = VariableDeclarationExpression(id.type, '*', id) + CORE.add_global(decl) + assignment = AssignmentExpression(None, None, id, rhs, obj) CORE.add(assignment) CORE.register_variable(id, obj) - obj.requires.append(assignment) return obj +def new_Pvariable(id, # type: ID + *args # type: Tuple[SafeExpType] + ): + rhs = id.type.new(*args) + return Pvariable(id, rhs) + + def add(expression, # type: Union[Expression, Statement] - require=True # type: bool ): # type: (...) -> None - CORE.add(expression, require=require) + CORE.add(expression) + + +def add_global(expression, # type: Union[Expression, Statement] + ): + # type: (...) -> None + CORE.add_global(expression) + + +def add_library(name, # type: str + version # type: Optional[str] + ): + # type: (...) -> None + CORE.add_library(Library(name, version)) + + +def add_build_flag(build_flag, # type: str + ): + # type: (...) -> None + CORE.add_build_flag(build_flag) + + +def add_define(name, # type: str + value=None, # type: Optional[SafeExpType] + ): + # type: (...) -> None + if value is None: + CORE.add_define(Define(name)) + else: + CORE.add_define(Define(name, safe_exp(value))) @coroutine @@ -407,12 +416,12 @@ def get_variable(id): # type: (ID) -> Generator[MockObj] @coroutine def process_lambda(value, # type: Lambda - parameters, # type: List[Tuple[Expression, str]] + parameters, # type: List[Tuple[SafeExpType, str]] capture='=', # type: str - return_type=None # type: Optional[Expression] + return_type=None # type: Optional[SafeExpType] ): # type: (...) -> Generator[LambdaExpression] - from esphome.components.globals import GlobalVariableComponent + from esphome.components.globals import GlobalsComponent if value is None: yield @@ -421,7 +430,7 @@ def process_lambda(value, # type: Lambda for i, id in enumerate(value.requires_ids): full_id, var = yield CORE.get_variable_with_full_id(id) if full_id is not None and isinstance(full_id.type, MockObjClass) and \ - full_id.type.inherits_from(GlobalVariableComponent): + full_id.type.inherits_from(GlobalsComponent): parts[i * 3 + 1] = var.value() continue @@ -433,13 +442,17 @@ def process_lambda(value, # type: Lambda yield LambdaExpression(parts, parameters, capture, return_type) +def is_template(value): + return isinstance(value, Lambda) + + @coroutine def templatable(value, # type: Any args, # type: List[Tuple[SafeExpType, str]] output_type, # type: Optional[SafeExpType], to_exp=None # type: Optional[Any] ): - if isinstance(value, Lambda): + if is_template(value): lambda_ = yield process_lambda(value, args, return_type=output_type) yield lambda_ else: @@ -452,47 +465,37 @@ def templatable(value, # type: Any class MockObj(Expression): - def __init__(self, base, op=u'.', has_side_effects=True): + def __init__(self, base, op=u'.'): self.base = base self.op = op - self._has_side_effects = has_side_effects super(MockObj, self).__init__() def __getattr__(self, attr): # type: (str) -> MockObj - if attr == u'_': - obj = MockObj(u'{}{}'.format(self.base, self.op)) - obj.requires.append(self) - return obj - if attr == u'new': - obj = MockObj(u'new {}'.format(self.base), u'->') - obj.requires.append(self) - return obj next_op = u'.' if attr.startswith(u'P') and self.op not in ['::', '']: attr = attr[1:] next_op = u'->' if attr.startswith(u'_'): attr = attr[1:] - obj = MockObj(u'{}{}{}'.format(self.base, self.op, attr), next_op) - obj.requires.append(self) - return obj + return MockObj(u'{}{}{}'.format(self.base, self.op, attr), next_op) def __call__(self, *args, **kwargs): # type: (*Any, **Any) -> MockObj call = CallExpression(self.base, *args) - obj = MockObj(call, self.op) - obj.requires.append(self) - obj.requires.append(call) - return obj + return MockObj(call, self.op) def __str__(self): # type: () -> unicode return text_type(self.base) - def require(self): # type: () -> None - self.required = True - for require in self.requires: - if require.required: - continue - require.require() + def __repr__(self): + return u'MockObj<{}>'.format(text_type(self.base)) + + @property + def _(self): + return MockObj(u'{}{}'.format(self.base, self.op)) + + @property + def new(self): + return MockObj(u'new {}'.format(self.base), u'->') def template(self, *args): # type: (Tuple[Union[TemplateArguments, Expression]]) -> MockObj if len(args) != 1 or not isinstance(args[0], TemplateArguments): @@ -500,19 +503,15 @@ class MockObj(Expression): else: args = args[0] obj = MockObj(u'{}{}'.format(self.base, args)) - obj.requires.append(self) - obj.requires.append(args) return obj def namespace(self, name): # type: (str) -> MockObj obj = MockObj(u'{}{}{}'.format(self.base, self.op, name), u'::') - obj.requires.append(self) return obj def class_(self, name, *parents): # type: (str, *MockObjClass) -> MockObjClass op = '' if self.op == '' else '::' obj = MockObjClass(u'{}{}{}'.format(self.base, op, name), u'.', parents=parents) - obj.requires.append(self) return obj def struct(self, name): # type: (str) -> MockObjClass @@ -521,37 +520,28 @@ class MockObj(Expression): def enum(self, name, is_class=False): # type: (str, bool) -> MockObj if is_class: return self.namespace(name) - return self def operator(self, name): # type: (str) -> MockObj if name == 'ref': - obj = MockObj(u'{} &'.format(self.base), u'') - obj.requires.append(self) - return obj + return MockObj(u'{} &'.format(self.base), u'') if name == 'ptr': - obj = MockObj(u'{} *'.format(self.base), u'') - obj.requires.append(self) - return obj + return MockObj(u'{} *'.format(self.base), u'') if name == "const": - obj = MockObj(u'const {}'.format(self.base), u'') - obj.requires.append(self) - return obj + return MockObj(u'const {}'.format(self.base), u'') raise NotImplementedError - def has_side_effects(self): # type: () -> bool - return self._has_side_effects + @property + def using(self): + assert self.op == '::' + return MockObj(u'using namespace {}'.format(self.base)) def __getitem__(self, item): # type: (Union[str, Expression]) -> MockObj next_op = u'.' if isinstance(item, str) and item.startswith(u'P'): item = item[1:] next_op = u'->' - obj = MockObj(u'{}[{}]'.format(self.base, item), next_op) - obj.requires.append(self) - if isinstance(item, Expression): - obj.requires.append(item) - return obj + return MockObj(u'{}[{}]'.format(self.base, item), next_op) class MockObjClass(MockObj): @@ -584,7 +574,7 @@ class MockObjClass(MockObj): args = args[0] new_parents = self._parents[:] new_parents.append(self) - obj = MockObjClass(u'{}{}'.format(self.base, args), parents=new_parents) - obj.requires.append(self) - obj.requires.append(args) - return obj + return MockObjClass(u'{}{}'.format(self.base, args), parents=new_parents) + + def __repr__(self): + return u'MockObjClass<{}, parents={}>'.format(text_type(self.base), self._parents) diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index feeac5107..e457d4529 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -1,61 +1,43 @@ -from esphome.const import CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_PCF8574, \ - CONF_SETUP_PRIORITY, CONF_MCP23017 -from esphome.core import CORE, EsphomeError, coroutine -from esphome.cpp_generator import IntLiteral, RawExpression -from esphome.cpp_types import GPIOInputPin, GPIOOutputPin +from esphome.const import CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_SETUP_PRIORITY +from esphome.core import coroutine +from esphome.cpp_generator import RawExpression, add +from esphome.cpp_types import App, GPIOPin @coroutine -def generic_gpio_pin_expression_(conf, mock_obj, default_mode): +def gpio_pin_expression(conf): if conf is None: return + from esphome import pins + for key, (_, func) in pins.PIN_SCHEMA_REGISTRY.items(): + if key in conf: + yield coroutine(func)(conf) + return + number = conf[CONF_NUMBER] + mode = conf[CONF_MODE] inverted = conf.get(CONF_INVERTED) - if CONF_PCF8574 in conf: - from esphome.components import pcf8574 - - hub = yield CORE.get_variable(conf[CONF_PCF8574]) - - if default_mode == u'INPUT': - mode = pcf8574.PCF8675_GPIO_MODES[conf.get(CONF_MODE, u'INPUT')] - yield hub.make_input_pin(number, mode, inverted) - return - if default_mode == u'OUTPUT': - yield hub.make_output_pin(number, inverted) - return - - raise EsphomeError(u"Unknown default mode {}".format(default_mode)) - if CONF_MCP23017 in conf: - from esphome.components import mcp23017 - - hub = yield CORE.get_variable(conf[CONF_MCP23017]) - - if default_mode == u'INPUT': - mode = mcp23017.MCP23017_GPIO_MODES[conf.get(CONF_MODE, u'INPUT')] - yield hub.make_input_pin(number, mode, inverted) - return - if default_mode == u'OUTPUT': - yield hub.make_output_pin(number, inverted) - return - - raise EsphomeError(u"Unknown default mode {}".format(default_mode)) - if len(conf) == 1: - yield IntLiteral(number) - return - mode = RawExpression(conf.get(CONF_MODE, default_mode)) - yield mock_obj(number, mode, inverted) + yield GPIOPin.new(number, RawExpression(mode), inverted) @coroutine -def gpio_output_pin_expression(conf): - yield generic_gpio_pin_expression_(conf, GPIOOutputPin, 'OUTPUT') - - -@coroutine -def gpio_input_pin_expression(conf): - yield generic_gpio_pin_expression_(conf, GPIOInputPin, 'INPUT') - - -def setup_component(obj, config): +def register_component(obj, config): if CONF_SETUP_PRIORITY in config: - CORE.add(obj.set_setup_priority(config[CONF_SETUP_PRIORITY])) + add(obj.set_setup_priority(config[CONF_SETUP_PRIORITY])) + add(App.register_component(obj)) + + +@coroutine +def build_registry_entry(registry, full_config): + key, config = next((k, v) for k, v in full_config.items() if k in registry) + builder = coroutine(registry[key][1]) + yield builder(config) + + +@coroutine +def build_registry_list(registry, config): + actions = [] + for conf in config: + action = yield build_registry_entry(registry, conf) + actions.append(action) + yield actions diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 01518fe80..4781faa77 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -2,6 +2,7 @@ from esphome.cpp_generator import MockObj global_ns = MockObj('', '') void = global_ns.namespace('void') +nullptr = global_ns.namespace('nullptr') float_ = global_ns.namespace('float') bool_ = global_ns.namespace('bool') std_ns = global_ns.namespace('std') @@ -15,7 +16,6 @@ const_char_ptr = global_ns.namespace('const char *') NAN = global_ns.namespace('NAN') esphome_ns = global_ns # using namespace esphome; App = esphome_ns.App -io_ns = esphome_ns.namespace('io') Nameable = esphome_ns.class_('Nameable') Trigger = esphome_ns.class_('Trigger') Action = esphome_ns.class_('Action') @@ -29,8 +29,5 @@ JsonObject = arduino_json_ns.class_('JsonObject') JsonObjectRef = JsonObject.operator('ref') JsonObjectConstRef = JsonObjectRef.operator('const') Controller = esphome_ns.class_('Controller') -StoringController = esphome_ns.class_('StoringController', Controller) GPIOPin = esphome_ns.class_('GPIOPin') -GPIOOutputPin = esphome_ns.class_('GPIOOutputPin', GPIOPin) -GPIOInputPin = esphome_ns.class_('GPIOInputPin', GPIOPin) diff --git a/esphome/espota.py b/esphome/espota.py deleted file mode 100755 index 9892c74eb..000000000 --- a/esphome/espota.py +++ /dev/null @@ -1,344 +0,0 @@ -#!/usr/bin/env python -# -# Copy of espota.py from ESP32 Arduino project with some modifications. -# -# Original espota.py by Ivan Grokhotkov: -# https://gist.github.com/igrr/d35ab8446922179dc58c -# -# Modified since 2015-09-18 from Pascal Gollor (https://github.com/pgollor) -# Modified since 2015-11-09 from Hristo Gochkov (https://github.com/me-no-dev) -# Modified since 2016-01-03 from Matthew O'Gorman (https://githumb.com/mogorman) -# -# This script will push an OTA update to the ESP -# use it like: python espota.py -i -I -p -P -# [-a password] -f -# Or to upload SPIFFS image: -# python espota.py -i -I -p -P [-a -# password] -s -f -# -# Changes -# 2018-03-29: -# - Clean up Code -# - Merge from esptool for ESP8266 -# 2015-09-18: -# - Add option parser. -# - Add logging -# - Send command to controller to differ between flashing and transmitting SPIFFS image. -# -# Changes -# 2015-11-09: -# - Added digest authentication -# - Enhanced error tracking and reporting -# -# Changes -# 2016-01-03: -# - Added more options to parser. -# - -import hashlib -import logging -# pylint: disable=deprecated-module -import optparse -import os -import random -import socket -import sys - -# pylint: disable=no-member - -# Commands -FLASH = 0 -SPIFFS = 100 -AUTH = 200 -PROGRESS = False - -_LOGGER = logging.getLogger(__name__) - - -def update_progress(progress): - """Displays or updates a console progress bar - - Accepts a float between 0 and 1. Any int will be converted to a float. - A value under 0 represents a 'halt'. A value at 1 or bigger represents 100%. - - :param progress: - :return: - """ - if PROGRESS: - bar_length = 60 # Modify this to change the length of the progress bar - status = "" - if isinstance(progress, int): - progress = float(progress) - if not isinstance(progress, float): - progress = 0 - status = "error: progress var must be float\r\n" - if progress < 0: - progress = 0 - status = "Halt...\r\n" - if progress >= 1: - progress = 1 - status = "Done...\r\n" - block = int(round(bar_length * progress)) - text = "\rUploading: [{0}] {1}% {2}".format("=" * block + " " * (bar_length - block), - int(progress * 100), status) - sys.stderr.write(text) - sys.stderr.flush() - else: - sys.stderr.write('.') - sys.stderr.flush() - - -def serve(remote_host, local_addr, remote_port, local_port, password, filename, command=FLASH): - # Create a TCP/IP socket - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server_address = (local_addr, local_port) - _LOGGER.info('Starting on %s:%s', server_address[0], server_address[1]) - try: - sock.bind(server_address) - sock.listen(1) - except Exception: # pylint: disable=broad-except - _LOGGER.error("Listen Failed") - return 1 - - content_size = os.path.getsize(filename) - f_handle = open(filename, 'rb') - file_md5 = hashlib.md5(f_handle.read()).hexdigest() - f_handle.close() - _LOGGER.info('Upload size: %d', content_size) - message = '%d %d %d %s\n' % (command, local_port, content_size, file_md5) - - # Wait for a connection - inv_trys = 0 - data = '' - msg = 'Sending invitation to {} '.format(remote_host) - _LOGGER.info(msg) - remote_address = (remote_host, int(remote_port)) - sock2 = None - while inv_trys < 10: - inv_trys += 1 - sock2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - sock2.sendto(message.encode(), remote_address) - except Exception: # pylint: disable=broad-except - _LOGGER.error('Failed') - sock2.close() - _LOGGER.error('Host %s Not Found', remote_host) - return 1 - sock2.settimeout(1) - try: - data = sock2.recv(37).decode() - break - except Exception: # pylint: disable=broad-except - sys.stderr.write('.') - sys.stderr.flush() - sock2.close() - sys.stderr.write('\n') - sys.stderr.flush() - if inv_trys == 10: - _LOGGER.error('No response from the ESP') - return 1 - if data != "OK": - if data.startswith('AUTH'): - nonce = data.split()[1] - cnonce_text = '%s%u%s%s' % (filename, content_size, file_md5, remote_host) - cnonce = hashlib.md5(cnonce_text.encode()).hexdigest() - passmd5 = hashlib.md5(password.encode()).hexdigest() - result_text = '%s:%s:%s' % (passmd5, nonce, cnonce) - result = hashlib.md5(result_text.encode()).hexdigest() - _LOGGER.info("Authenticating...") - message = '%d %s %s\n' % (AUTH, cnonce, result) - sock2.sendto(message.encode(), remote_address) - sock2.settimeout(10) - try: - data = sock2.recv(32).decode() - except Exception: # pylint: disable=broad-except - _LOGGER.error('FAIL: No Answer to our Authentication') - sock2.close() - return 1 - if data != "OK": - _LOGGER.error('FAIL: %s', data) - sock2.close() - return 1 - _LOGGER.info('OK') - else: - _LOGGER.error('Bad Answer: %s', data) - sock2.close() - return 1 - sock2.close() - - _LOGGER.info('Waiting for device...') - try: - sock.settimeout(10) - connection, _ = sock.accept() - sock.settimeout(None) - connection.settimeout(None) - except Exception: # pylint: disable=broad-except - _LOGGER.error('No response from device') - sock.close() - return 1 - - try: - f_handle = open(filename, "rb") - if PROGRESS: - update_progress(0) - else: - _LOGGER.info('Uploading...') - offset = 0 - while True: - chunk = f_handle.read(1024) - if not chunk: - break - offset += len(chunk) - update_progress(offset / float(content_size)) - connection.settimeout(10) - try: - connection.sendall(chunk) - connection.recv(10) - except Exception: # pylint: disable=broad-except - sys.stderr.write('\n') - _LOGGER.error('Error Uploading') - connection.close() - f_handle.close() - sock.close() - return 1 - - sys.stderr.write('\n') - _LOGGER.info('Waiting for result...') - try: - connection.settimeout(60) - while True: - if connection.recv(32).decode().find('O') >= 0: - break - _LOGGER.info('Result: OK') - connection.close() - f_handle.close() - sock.close() - if data != "OK": - _LOGGER.error('%s', data) - return 1 - except Exception: # pylint: disable=broad-except - _LOGGER.error('No Result!') - connection.close() - f_handle.close() - sock.close() - return 1 - - finally: - connection.close() - f_handle.close() - - return 0 - - -def parse_args(unparsed_args): - parser = optparse.OptionParser( - usage="%prog [options]", - description="Transmit image over the air to the esp8266 module with OTA support." - ) - - # destination ip and port - group = optparse.OptionGroup(parser, "Destination") - group.add_option( - "-i", "--ip", - dest="esp_ip", - action="store", - help="ESP8266 IP Address.", - default=False - ) - group.add_option( - "-I", "--host_ip", - dest="host_ip", - action="store", - help="Host IP Address.", - default="0.0.0.0" - ) - group.add_option( - "-p", "--port", - dest="esp_port", - type="int", - help="ESP8266 ota Port. Default 8266", - default=8266 - ) - group.add_option( - "-P", "--host_port", - dest="host_port", - type="int", - help="Host server ota Port. Default random 10000-60000", - default=random.randint(10000, 60000) - ) - parser.add_option_group(group) - - # auth - group = optparse.OptionGroup(parser, "Authentication") - group.add_option( - "-a", "--auth", - dest="auth", - help="Set authentication password.", - action="store", - default="" - ) - parser.add_option_group(group) - - # image - group = optparse.OptionGroup(parser, "Image") - group.add_option( - "-f", "--file", - dest="image", - help="Image file.", - metavar="FILE", - default=None - ) - group.add_option( - "-s", "--spiffs", - dest="spiffs", - action="store_true", - help="Use this option to transmit a SPIFFS image and do not flash the " - "module.", - default=False - ) - parser.add_option_group(group) - - # output group - group = optparse.OptionGroup(parser, "Output") - group.add_option( - "-d", "--debug", - dest="debug", - help="Show debug output. And override loglevel with debug.", - action="store_true", - default=False - ) - group.add_option( - "-r", "--progress", - dest="progress", - help="Show progress output. Does not work for ArduinoIDE", - action="store_true", - default=False - ) - parser.add_option_group(group) - - options, _ = parser.parse_args(unparsed_args) - - return options - - -def main(args): - options = parse_args(args) - _LOGGER.debug("Options: %s", str(options)) - - # check options - global PROGRESS - PROGRESS = options.progress - if not options.esp_ip or not options.image: - _LOGGER.critical("Not enough arguments.") - return 1 - - command = FLASH - if options.spiffs: - command = SPIFFS - - return serve(options.esp_ip, options.host_ip, options.esp_port, options.host_port, - options.auth, options.image, command) - - -if __name__ == '__main__': - sys.exit(main(sys.argv)) diff --git a/esphome/espota2.py b/esphome/espota2.py index ac0dd33eb..1a766d4eb 100755 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -297,15 +297,3 @@ def run_ota(remote_host, remote_port, password, filename): return run_ota_impl_(remote_host, remote_port, password, filename) except OTAError as err: _LOGGER.error(err) - return 1 - - -def run_legacy_ota(verbose, host_port, remote_host, remote_port, password, filename): - from esphome import espota - - espota_args = ['espota.py', '--debug', '--progress', '-i', remote_host, - '-p', str(remote_port), '-f', filename, - '-a', password, '-P', str(host_port)] - if verbose: - espota_args.append('-d') - return espota.main(espota_args) diff --git a/esphome/helpers.py b/esphome/helpers.py index 8c0767799..0c0157468 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -1,5 +1,6 @@ from __future__ import print_function +import codecs import logging import os @@ -143,3 +144,41 @@ def get_bool_env(var, default=False): def is_hassio(): return get_bool_env('ESPHOME_IS_HASSIO') + + +def copy_file_if_changed(src, dst): + src_text = read_file(src) + if os.path.isfile(dst): + dst_text = read_file(dst) + else: + dst_text = None + if src_text == dst_text: + return + write_file(dst, src_text) + + +def read_file(path): + try: + with codecs.open(path, 'r', encoding='utf-8') as f_handle: + return f_handle.read() + except OSError: + from esphome.core import EsphomeError + raise EsphomeError(u"Could not read file at {}".format(path)) + + +def write_file(path, text): + try: + mkdir_p(os.path.dirname(path)) + with codecs.open(path, 'w+', encoding='utf-8') as f_handle: + f_handle.write(text) + except OSError: + from esphome.core import EsphomeError + raise EsphomeError(u"Could not write file at {}".format(path)) + + +def write_file_if_changed(text, dst): + src_content = None + if os.path.isfile(dst): + src_content = read_file(dst) + if src_content != text: + write_file(dst, text) diff --git a/esphome/pins.py b/esphome/pins.py index b26669a8a..1576e423a 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -2,12 +2,10 @@ from __future__ import division import logging -import voluptuous as vol - import esphome.config_validation as cv -from esphome.const import CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_PCF8574, CONF_MCP23017 +from esphome.const import CONF_INVERTED, CONF_MODE, CONF_NUMBER from esphome.core import CORE -from esphome.cpp_types import Component, esphome_ns, io_ns +from esphome.util import ServiceRegistry _LOGGER = logging.getLogger(__name__) @@ -59,7 +57,7 @@ ESP8266_BOARD_PINS = { 'xinabox_cw01': {'SDA': 2, 'SCL': 14, 'LED': 5, 'LED_RED': 12, 'LED_GREEN': 13} } -FLASH_SIZE_1_MB = 2**20 +FLASH_SIZE_1_MB = 2 ** 20 FLASH_SIZE_512_KB = FLASH_SIZE_1_MB // 2 FLASH_SIZE_2_MB = 2 * FLASH_SIZE_1_MB FLASH_SIZE_4_MB = 4 * FLASH_SIZE_1_MB @@ -247,13 +245,13 @@ def _lookup_pin(value): return board_pins[value] if value in base_pins: return base_pins[value] - raise vol.Invalid(u"Cannot resolve pin name '{}' for board {}.".format(value, CORE.board)) + raise cv.Invalid(u"Cannot resolve pin name '{}' for board {}.".format(value, CORE.board)) def _translate_pin(value): if isinstance(value, dict) or value is None: - raise vol.Invalid(u"This variable only supports pin numbers, not full pin schemas " - u"(with inverted and mode).") + raise cv.Invalid(u"This variable only supports pin numbers, not full pin schemas " + u"(with inverted and mode).") if isinstance(value, int): return value try: @@ -261,7 +259,7 @@ def _translate_pin(value): except ValueError: pass if value.startswith('GPIO'): - return vol.Coerce(int)(value[len('GPIO'):].strip()) + return cv.Coerce(int)(value[len('GPIO'):].strip()) return _lookup_pin(value) @@ -269,7 +267,7 @@ def validate_gpio_pin(value): value = _translate_pin(value) if CORE.is_esp32: if value < 0 or value > 39: - raise vol.Invalid(u"ESP32: Invalid pin number: {}".format(value)) + raise cv.Invalid(u"ESP32: Invalid pin number: {}".format(value)) if 6 <= value <= 11: _LOGGER.warning(u"ESP32: Pin %s (6-11) might already be used by the " u"flash interface. Be warned.", value) @@ -282,7 +280,7 @@ def validate_gpio_pin(value): _LOGGER.warning(u"ESP8266: Pin %s (6-11) might already be used by the " u"flash interface. Be warned.", value) if value < 0 or value > 17: - raise vol.Invalid(u"ESP8266: Invalid pin number: {}".format(value)) + raise cv.Invalid(u"ESP8266: Invalid pin number: {}".format(value)) return value raise NotImplementedError @@ -297,8 +295,8 @@ def input_pullup_pin(value): return output_pin(value) if CORE.is_esp8266: if value == 0: - raise vol.Invalid("GPIO Pin 0 does not support pullup pin mode. " - "Please choose another pin.") + raise cv.Invalid("GPIO Pin 0 does not support pullup pin mode. " + "Please choose another pin.") return value raise NotImplementedError @@ -307,8 +305,8 @@ def output_pin(value): value = validate_gpio_pin(value) if CORE.is_esp32: if 34 <= value <= 39: - raise vol.Invalid(u"ESP32: GPIO{} (34-39) can only be used as an " - u"input pin.".format(value)) + raise cv.Invalid(u"ESP32: GPIO{} (34-39) can only be used as an " + u"input pin.".format(value)) return value if CORE.is_esp8266: return value @@ -320,15 +318,15 @@ def analog_pin(value): if CORE.is_esp32: if 32 <= value <= 39: # ADC1 return value - raise vol.Invalid(u"ESP32: Only pins 32 though 39 support ADC.") + raise cv.Invalid(u"ESP32: Only pins 32 though 39 support ADC.") if CORE.is_esp8266: if value == 17: # A0 return value - raise vol.Invalid(u"ESP8266: Only pin A0 (17) supports ADC.") + raise cv.Invalid(u"ESP8266: Only pin A0 (17) supports ADC.") raise NotImplementedError -input_output_pin = vol.All(input_pin, output_pin) +input_output_pin = cv.All(input_pin, output_pin) PIN_MODES_ESP8266 = [ 'INPUT', 'OUTPUT', 'INPUT_PULLUP', 'OUTPUT_OPEN_DRAIN', 'SPECIAL', 'FUNCTION_1', @@ -352,66 +350,61 @@ def pin_mode(value): GPIO_FULL_OUTPUT_PIN_SCHEMA = cv.Schema({ - vol.Required(CONF_NUMBER): output_pin, - vol.Optional(CONF_MODE): pin_mode, - vol.Optional(CONF_INVERTED): cv.boolean, + cv.Required(CONF_NUMBER): output_pin, + cv.Optional(CONF_MODE, default='OUTPUT'): pin_mode, + cv.Optional(CONF_INVERTED, default=False): cv.boolean, }) GPIO_FULL_INPUT_PIN_SCHEMA = cv.Schema({ - vol.Required(CONF_NUMBER): input_pin, - vol.Optional(CONF_MODE): pin_mode, - vol.Optional(CONF_INVERTED): cv.boolean, + cv.Required(CONF_NUMBER): input_pin, + cv.Optional(CONF_MODE, default='INPUT'): pin_mode, + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) + +GPIO_FULL_INPUT_PULLUP_PIN_SCHEMA = cv.Schema({ + cv.Required(CONF_NUMBER): input_pin, + cv.Optional(CONF_MODE, default='INPUT_PULLUP'): pin_mode, + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) + +GPIO_FULL_ANALOG_PIN_SCHEMA = cv.Schema({ + cv.Required(CONF_NUMBER): analog_pin, + cv.Optional(CONF_MODE, default='INPUT'): pin_mode, }) def shorthand_output_pin(value): value = output_pin(value) - return {CONF_NUMBER: value} + return GPIO_FULL_OUTPUT_PIN_SCHEMA({CONF_NUMBER: value}) def shorthand_input_pin(value): value = input_pin(value) - return {CONF_NUMBER: value} + return GPIO_FULL_INPUT_PIN_SCHEMA({CONF_NUMBER: value}) def shorthand_input_pullup_pin(value): value = input_pullup_pin(value) - return {CONF_NUMBER: value} + return GPIO_FULL_INPUT_PIN_SCHEMA({ + CONF_NUMBER: value, + CONF_MODE: 'INPUT_PULLUP', + }) + + +def shorthand_analog_pin(value): + value = analog_pin(value) + return GPIO_FULL_INPUT_PIN_SCHEMA({CONF_NUMBER: value}) def validate_has_interrupt(value): if CORE.is_esp8266: if value[CONF_NUMBER] >= 16: - raise vol.Invalid("Pins GPIO16 and GPIO17 do not support interrupts and cannot be used " - "here, got {}".format(value[CONF_NUMBER])) + raise cv.Invalid("Pins GPIO16 and GPIO17 do not support interrupts and cannot be used " + "here, got {}".format(value[CONF_NUMBER])) return value -I2CDevice = esphome_ns.class_('I2CDevice') -PCF8574Component = io_ns.class_('PCF8574Component', Component, I2CDevice) -MCP23017 = io_ns.class_('MCP23017', Component, I2CDevice) - -PCF8574_OUTPUT_PIN_SCHEMA = cv.Schema({ - vol.Required(CONF_PCF8574): cv.use_variable_id(PCF8574Component), - vol.Required(CONF_NUMBER): vol.Coerce(int), - vol.Optional(CONF_MODE): cv.one_of("OUTPUT", upper=True), - vol.Optional(CONF_INVERTED, default=False): cv.boolean, -}) - -PCF8574_INPUT_PIN_SCHEMA = PCF8574_OUTPUT_PIN_SCHEMA.extend({ - vol.Optional(CONF_MODE): cv.one_of("INPUT", "INPUT_PULLUP", upper=True), -}) - -MCP23017_OUTPUT_PIN_SCHEMA = cv.Schema({ - vol.Required(CONF_MCP23017): cv.use_variable_id(MCP23017), - vol.Required(CONF_NUMBER): vol.All(vol.Coerce(int), vol.Range(min=0, max=15)), - vol.Optional(CONF_MODE): cv.one_of("OUTPUT", upper=True), - vol.Optional(CONF_INVERTED, default=False): cv.boolean, -}) - -MCP23017_INPUT_PIN_SCHEMA = MCP23017_OUTPUT_PIN_SCHEMA.extend({ - vol.Optional(CONF_MODE): cv.one_of("INPUT", "INPUT_PULLUP", upper=True), -}) +PIN_SCHEMA_REGISTRY = ServiceRegistry() def internal_gpio_output_pin_schema(value): @@ -421,10 +414,10 @@ def internal_gpio_output_pin_schema(value): def gpio_output_pin_schema(value): - if isinstance(value, dict) and CONF_PCF8574 in value: - return PCF8574_OUTPUT_PIN_SCHEMA(value) - if isinstance(value, dict) and CONF_MCP23017 in value: - return MCP23017_OUTPUT_PIN_SCHEMA(value) + if isinstance(value, dict): + for key, ((output_validator, _), _) in PIN_SCHEMA_REGISTRY.items(): + if key in value: + return output_validator(value) return internal_gpio_output_pin_schema(value) @@ -434,23 +427,29 @@ def internal_gpio_input_pin_schema(value): return shorthand_input_pin(value) +def internal_gpio_analog_pin_schema(value): + if isinstance(value, dict): + return GPIO_FULL_ANALOG_PIN_SCHEMA(value) + return shorthand_analog_pin(value) + + def gpio_input_pin_schema(value): - if isinstance(value, dict) and CONF_PCF8574 in value: - return PCF8574_INPUT_PIN_SCHEMA(value) - if isinstance(value, dict) and CONF_MCP23017 in value: - return MCP23017_INPUT_PIN_SCHEMA(value) + if isinstance(value, dict): + for key, ((_, input_validator), _) in PIN_SCHEMA_REGISTRY.items(): + if key in value: + return input_validator(value) return internal_gpio_input_pin_schema(value) def internal_gpio_input_pullup_pin_schema(value): if isinstance(value, dict): - return GPIO_FULL_INPUT_PIN_SCHEMA(value) + return GPIO_FULL_INPUT_PULLUP_PIN_SCHEMA(value) return shorthand_input_pullup_pin(value) def gpio_input_pullup_pin_schema(value): - if isinstance(value, dict) and CONF_PCF8574 in value: - return PCF8574_INPUT_PIN_SCHEMA(value) - if isinstance(value, dict) and CONF_MCP23017 in value: - return MCP23017_INPUT_PIN_SCHEMA(value) - return internal_gpio_input_pin_schema(value) + if isinstance(value, dict): + for key, ((_, input_validator), _) in PIN_SCHEMA_REGISTRY.items(): + if key in value: + return input_validator(value) + return internal_gpio_input_pullup_pin_schema(value) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index c91679062..cafb8ebaf 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -12,6 +12,50 @@ from esphome.util import run_external_command, run_external_process _LOGGER = logging.getLogger(__name__) +def patch_structhash(): + # Patch platformio's structhash to not recompile the entire project when files are + # removed/added. This might have unintended consequences, but this improves compile + # times greatly when adding/removing components and a simple clean build solves + # all issues + from platformio.commands import run + from platformio import util + from os.path import join, isdir, getmtime, isfile + from os import makedirs + + def patched_clean_build_dir(build_dir): + structhash_file = join(build_dir, "structure.hash") + platformio_ini = join(util.get_project_dir(), "platformio.ini") + + # if project's config is modified + if isdir(build_dir) and getmtime(platformio_ini) > getmtime(build_dir): + util.rmtree_(build_dir) + + if not isdir(build_dir): + makedirs(build_dir) + + proj_hash = run.calculate_project_hash() + + # check project structure + if isdir(build_dir) and isfile(structhash_file): + with open(structhash_file) as f: + if f.read() == proj_hash: + return + + with open(structhash_file, "w") as f: + f.write(proj_hash) + + # pylint: disable=protected-access + orig = run._clean_build_dir + + def patched_safe(*args, **kwargs): + try: + return patched_clean_build_dir(*args, **kwargs) + except Exception: # pylint: disable=broad-except + return orig(*args, **kwargs) + + run._clean_build_dir = patched_safe + + def run_platformio_cli(*args, **kwargs): os.environ["PLATFORMIO_FORCE_COLOR"] = "true" os.environ["PLATFORMIO_BUILD_DIR"] = os.path.abspath(CORE.relative_pioenvs_path()) @@ -20,6 +64,11 @@ def run_platformio_cli(*args, **kwargs): if os.environ.get('ESPHOME_USE_SUBPROCESS') is None: import platformio.__main__ + try: + patch_structhash() + except Exception: # pylint: disable=broad-except + # Ignore when patch fails + pass return run_external_command(platformio.__main__.main, *cmd, **kwargs) diff --git a/esphome/py_compat.py b/esphome/py_compat.py index 4b48aa0f8..ed6d7dd22 100644 --- a/esphome/py_compat.py +++ b/esphome/py_compat.py @@ -74,8 +74,8 @@ def indexbytes(buf, i): if IS_PY2: def decode_text(data, encoding='utf-8', errors='strict'): # type: (str, str, str) -> unicode - return unicode(data, encoding='utf-8', errors=errors) + return unicode(data, encoding=encoding, errors=errors) else: def decode_text(data, encoding='utf-8', errors='strict'): # type: (bytes, str, str) -> str - return data.decode(encoding='utf-8', errors=errors) + return data.decode(encoding=encoding, errors=errors) diff --git a/esphome/storage_json.py b/esphome/storage_json.py index 7187afff0..a443692ac 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -35,17 +35,14 @@ def trash_storage_path(base_path): # type: (str) -> str # pylint: disable=too-many-instance-attributes class StorageJSON(object): - def __init__(self, storage_version, name, esphome_core_version, esphome_version, + def __init__(self, storage_version, name, esphome_version, src_version, arduino_version, address, esp_platform, board, build_path, - firmware_bin_path, use_legacy_ota): + firmware_bin_path): # Version of the storage JSON schema assert storage_version is None or isinstance(storage_version, int) self.storage_version = storage_version # type: int # The name of the node self.name = name # type: str - # The esphome-core version in use - assert esphome_core_version is None or isinstance(esphome_core_version, dict) - self.esphome_core_version = esphome_core_version # type: Dict[str, str] # The esphome version this was compiled with self.esphome_version = esphome_version # type: str # The version of the file in src/main.cpp - Used to migrate the file @@ -64,14 +61,11 @@ class StorageJSON(object): self.build_path = build_path # type: str # The absolute path to the firmware binary self.firmware_bin_path = firmware_bin_path # type: str - # Whether to use legacy OTA, will be off after the first successful flash - self.use_legacy_ota = use_legacy_ota def as_dict(self): return { 'storage_version': self.storage_version, 'name': self.name, - 'esphome_core_version': self.esphome_core_version, 'esphome_version': self.esphome_version, 'src_version': self.src_version, 'arduino_version': self.arduino_version, @@ -80,7 +74,6 @@ class StorageJSON(object): 'board': self.board, 'build_path': self.build_path, 'firmware_bin_path': self.firmware_bin_path, - 'use_legacy_ota': self.use_legacy_ota, } def to_json(self): @@ -96,7 +89,6 @@ class StorageJSON(object): return StorageJSON( storage_version=1, name=esph.name, - esphome_core_version=esph.esphome_core_version, esphome_version=const.__version__, src_version=1, arduino_version=esph.arduino_version, @@ -105,7 +97,6 @@ class StorageJSON(object): board=esph.board, build_path=esph.build_path, firmware_bin_path=esph.firmware_bin, - use_legacy_ota=True if old is None else old.use_legacy_ota, ) @staticmethod @@ -114,7 +105,6 @@ class StorageJSON(object): return StorageJSON( storage_version=1, name=name, - esphome_core_version=None, esphome_version=const.__version__, src_version=1, arduino_version=None, @@ -123,7 +113,6 @@ class StorageJSON(object): board=board, build_path=None, firmware_bin_path=None, - use_legacy_ota=False, ) @staticmethod @@ -133,8 +122,6 @@ class StorageJSON(object): storage = json.loads(text, encoding='utf-8') storage_version = storage['storage_version'] name = storage.get('name') - esphome_core_version = storage.get('esphome_core_version', - storage.get('esphomelib_version')) esphome_version = storage.get('esphome_version', storage.get('esphomeyaml_version')) src_version = storage.get('src_version') arduino_version = storage.get('arduino_version') @@ -143,10 +130,9 @@ class StorageJSON(object): board = storage.get('board') build_path = storage.get('build_path') firmware_bin_path = storage.get('firmware_bin_path') - use_legacy_ota = storage.get('use_legacy_ota') - return StorageJSON(storage_version, name, esphome_core_version, esphome_version, + return StorageJSON(storage_version, name, esphome_version, src_version, arduino_version, address, esp_platform, board, build_path, - firmware_bin_path, use_legacy_ota) + firmware_bin_path) @staticmethod def load(path): # type: (str) -> Optional[StorageJSON] diff --git a/esphome/util.py b/esphome/util.py index 3f01f0772..02ecc14d1 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -1,5 +1,6 @@ from __future__ import print_function +import collections import io import logging import re @@ -7,6 +8,7 @@ import subprocess import sys from esphome import const +from esphome.py_compat import IS_PY2 _LOGGER = logging.getLogger(__name__) @@ -153,3 +155,30 @@ def run_external_process(*cmd, **kwargs): def is_dev_esphome_version(): return 'dev' in const.__version__ + + +# Custom OrderedDict with nicer repr method for debugging +class OrderedDict(collections.OrderedDict): + def __repr__(self): + return dict(self).__repr__() + + def move_to_end(self, key, last=True): + if IS_PY2: + if last: + # When moving to end, just pop and re-add + val = self.pop(key) + self[key] = val + else: + # When moving to front, use internals here + # https://stackoverflow.com/a/16664932 + root = self._OrderedDict__root # pylint: disable=no-member + first = root[1] + link = self._OrderedDict__map[key] # pylint: disable=no-member + link_prev, link_next, _ = link + link_prev[1] = link_next + link_next[0] = link_prev + link[0] = root + link[1] = first + root[1] = first[0] = link + else: + super(OrderedDict, self).move_to_end(key, last=last) # pylint: disable=no-member diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index 924952ded..6e5cbda84 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -161,5 +161,7 @@ class _Schema(vol.Schema): return validate_mapping def extend(self, schema, required=None, extra=None): + if isinstance(schema, vol.Schema): + schema = schema.schema ret = vol.Schema.extend(self, schema, required=required, extra=extra) return _Schema(ret.schema, required=ret.required, extra=ret.extra) diff --git a/esphome/wizard.py b/esphome/wizard.py index 091e364ac..ac7791549 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -7,7 +7,6 @@ import unicodedata import voluptuous as vol import esphome.config_validation as cv -from esphome.const import ESP_PLATFORMS, ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266 from esphome.helpers import color, get_bool_env # pylint: disable=anomalous-backslash-in-string from esphome.pins import ESP32_BOARD_PINS, ESP8266_BOARD_PINS @@ -177,7 +176,7 @@ def wizard(path): safe_print("Please enter either ESP32 or ESP8266.") platform = safe_input(color("bold_white", "(ESP32/ESP8266): ")) try: - platform = vol.All(vol.Upper, vol.Any(*ESP_PLATFORMS))(platform) + platform = vol.All(vol.Upper, vol.Any('ESP32', 'ESP8266'))(platform) break except vol.Invalid: safe_print(u"Unfortunately, I can't find an espressif microcontroller called " @@ -186,7 +185,7 @@ def wizard(path): safe_print() sleep(1) - if platform == ESP_PLATFORM_ESP32: + if platform == 'ESP32': board_link = 'http://docs.platformio.org/en/latest/platforms/espressif32.html#boards' else: board_link = 'http://docs.platformio.org/en/latest/platforms/espressif8266.html#boards' @@ -194,11 +193,11 @@ def wizard(path): safe_print("Next, I need to know what " + color('green', 'board') + " you're using.") sleep(0.5) safe_print("Please go to {} and choose a board.".format(color('green', board_link))) - if platform == ESP_PLATFORM_ESP8266: + if platform == 'ESP32': safe_print("(Type " + color('green', 'esp01_1m') + " for Sonoff devices)") safe_print() # Don't sleep because user needs to copy link - if platform == ESP_PLATFORM_ESP32: + if platform == 'ESP32': safe_print("For example \"{}\".".format(color("bold_white", 'nodemcu-32s'))) boards = list(ESP32_BOARD_PINS.keys()) else: diff --git a/esphome/writer.py b/esphome/writer.py index 12bc2ed9b..5d20e8e18 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -1,22 +1,15 @@ from __future__ import print_function -import codecs import logging import os import re from esphome.config import iter_components -from esphome.const import ARDUINO_VERSION_ESP32_1_0_0, ARDUINO_VERSION_ESP8266_2_5_0, \ - ARDUINO_VERSION_ESP8266_DEV, CONF_BOARD_FLASH_MODE, CONF_BRANCH, CONF_COMMIT, CONF_ESPHOME, \ - CONF_LOCAL, CONF_PLATFORMIO_OPTIONS, CONF_REPOSITORY, CONF_TAG, CONF_USE_CUSTOM_CODE, \ - ARDUINO_VERSION_ESP8266_2_3_0 +from esphome.const import CONF_BOARD_FLASH_MODE, CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS, \ + HEADER_FILE_EXTENSIONS, SOURCE_FILE_EXTENSIONS from esphome.core import CORE, EsphomeError -from esphome.core_config import GITHUB_ARCHIVE_ZIP, LIBRARY_URI_REPO, VERSION_REGEX -from esphome.helpers import mkdir_p, run_system_command -from esphome.pins import ESP8266_FLASH_SIZES, ESP8266_LD_SCRIPTS -from esphome.py_compat import IS_PY3, string_types +from esphome.helpers import mkdir_p, read_file, write_file_if_changed from esphome.storage_json import StorageJSON, storage_path -from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -60,20 +53,11 @@ UPLOAD_SPEED_OVERRIDE = { } -def get_build_flags(key): - build_flags = set() +def get_flags(key): + flags = set() for _, component, conf in iter_components(CORE.config): - if not hasattr(component, key): - continue - flags = getattr(component, key) - if callable(flags): - flags = flags(conf) - if flags is None: - continue - if isinstance(flags, string_types): - flags = [flags] - build_flags |= set(flags) - return build_flags + flags |= getattr(component, key)(conf) + return flags def get_include_text(): @@ -95,37 +79,6 @@ def get_include_text(): return include_text -def update_esphome_core_repo(): - if CONF_REPOSITORY not in CORE.esphome_core_version: - return - - if CONF_BRANCH not in CORE.esphome_core_version: - # Git commit hash or tag cannot be updated - return - - esphome_core_path = CORE.relative_piolibdeps_path('esphome-core') - - rc, _, _ = run_system_command('git', '-C', esphome_core_path, '--help') - if rc != 0: - # git not installed or repo not downloaded yet - return - rc, _, _ = run_system_command('git', '-C', esphome_core_path, 'diff-index', '--quiet', 'HEAD', - '--') - if rc != 0: - # local changes, cannot update - _LOGGER.warning("Local changes in esphome-core copy from git. Will not auto-update.") - return - _LOGGER.info("Updating esphome-core copy from git (%s)", esphome_core_path) - rc, stdout, _ = run_system_command('git', '-c', 'color.ui=always', '-C', esphome_core_path, - 'pull', '--stat') - if rc != 0: - _LOGGER.warning("Couldn't auto-update local git copy of esphome-core.") - return - if IS_PY3: - stdout = stdout.decode('utf-8', 'backslashreplace') - safe_print(stdout.strip()) - - def replace_file_content(text, pattern, repl): content_new, count = re.subn(pattern, repl, text, flags=re.M) return content_new, count @@ -136,8 +89,7 @@ def migrate_src_version_0_to_1(): if not os.path.isfile(main_cpp): return - with codecs.open(main_cpp, 'r', encoding='utf-8') as f_handle: - content = orig_content = f_handle.read() + content = read_file(main_cpp) if CPP_INCLUDE_BEGIN in content: return @@ -160,10 +112,7 @@ def migrate_src_version_0_to_1(): "auto-generated again.", main_cpp, main_cpp) _LOGGER.info("Migration: Added include section to %s", main_cpp) - if orig_content == content: - return - with codecs.open(main_cpp, 'w', encoding='utf-8') as f_handle: - f_handle.write(content) + write_file_if_changed(content, main_cpp) def migrate_src_version(old, new): @@ -181,8 +130,6 @@ def storage_should_clean(old, new): # type: (StorageJSON, StorageJSON) -> bool if old is None: return True - if old.esphome_core_version != new.esphome_core_version: - return True if old.esphome_version != new.esphome_version: return True if old.src_version != new.src_version: @@ -213,29 +160,6 @@ def update_storage_json(): new.save(path) -def symlink_esphome_core_version(esphome_core_version): - from esphome.symlink_ops import symlink, islink, readlink, unlink - - lib_path = CORE.relative_build_path('lib') - dst_path = CORE.relative_build_path('lib', 'esphome-core') - if CORE.is_local_esphome_core_copy: - src_path = CORE.relative_path(esphome_core_version[CONF_LOCAL]) - do_write = True - if islink(dst_path): - old_path = os.path.join(readlink(dst_path), lib_path) - if old_path != lib_path: - unlink(dst_path) - else: - do_write = False - if do_write: - mkdir_p(lib_path) - symlink(src_path, dst_path) - else: - # Remove symlink when changing back from local version - if islink(dst_path): - unlink(dst_path) - - def format_ini(data): content = u'' for key, value in sorted(data.items()): @@ -249,119 +173,13 @@ def format_ini(data): def gather_lib_deps(): - import json - - lib_deps = set() - if CONF_REPOSITORY in CORE.esphome_core_version: - repo = CORE.esphome_core_version[CONF_REPOSITORY] - ref = next((CORE.esphome_core_version[x] for x in (CONF_COMMIT, CONF_BRANCH, CONF_TAG) - if x in CORE.esphome_core_version), None) - if CONF_TAG in CORE.esphome_core_version and repo == LIBRARY_URI_REPO: - this_version = GITHUB_ARCHIVE_ZIP.format(ref) - elif ref is not None: - this_version = repo + '#' + ref - lib_deps.add(this_version) - elif CORE.is_local_esphome_core_copy: - src_path = CORE.relative_path(CORE.esphome_core_version[CONF_LOCAL]) - # Manually add lib_deps because platformio seems to ignore them inside libs/ - library_json_path = os.path.join(src_path, 'library.json') - with codecs.open(library_json_path, 'r', encoding='utf-8') as f_handle: - library_json_text = f_handle.read() - - library_json = json.loads(library_json_text) - for dep in library_json.get('dependencies', []): - if 'version' in dep and VERSION_REGEX.match(dep['version']) is not None: - lib_deps.add(dep['name'] + '@' + dep['version']) - else: - lib_deps.add(dep['version']) - else: - lib_deps.add(CORE.esphome_core_version) - - lib_deps |= get_build_flags('LIB_DEPS') - lib_deps |= get_build_flags('lib_deps') - if CORE.is_esp32: - lib_deps |= { - 'Preferences', # Preferences helper - 'AsyncTCP@1.0.3', # Pin AsyncTCP version - } - - # Manual fix for AsyncTCP - if CORE.arduino_version == ARDUINO_VERSION_ESP32_1_0_0: - lib_deps.discard('AsyncTCP@1.0.3') - lib_deps.add('AsyncTCP@1.0.1') - lib_deps.add('ESPmDNS') - elif CORE.is_esp8266: - lib_deps.add('ESPAsyncTCP@1.2.0') - lib_deps.add('ESP8266mDNS') - - # avoid changing build flags order - lib_deps_l = list(lib_deps) + lib_deps_l = [x.as_lib_dep for x in CORE.libraries] lib_deps_l.sort() - - # Move AsyncTCP to front, see https://github.com/platformio/platformio-core/issues/2115 - if 'AsyncTCP@1.0.3' in lib_deps_l: - lib_deps_l.insert(0, lib_deps_l.pop(lib_deps_l.index('AsyncTCP@1.0.3'))) - if 'AsyncTCP@1.0.1' in lib_deps_l: - lib_deps_l.insert(0, lib_deps_l.pop(lib_deps_l.index('AsyncTCP@1.0.1'))) - return lib_deps_l def gather_build_flags(): - build_flags = set() - if not CORE.config[CONF_ESPHOME][CONF_USE_CUSTOM_CODE]: - build_flags |= get_build_flags('build_flags') - build_flags |= get_build_flags('BUILD_FLAGS') - build_flags.add('-DESPHOME_USE') - build_flags.add("-Wno-unused-variable") - build_flags.add("-Wno-unused-but-set-variable") - build_flags.add("-Wno-sign-compare") - build_flags |= get_build_flags('required_build_flags') - build_flags |= get_build_flags('REQUIRED_BUILD_FLAGS') - - if not CORE.config[CONF_ESPHOME][CONF_USE_CUSTOM_CODE]: - # For new users, include common components out of the box. - # So that first impression is improved and user doesn't need to wait - # an eternity. - # It's not a perfect solution but shouldn't cause any issues I think - # Common components determined through Google Analytics page views - # and only components that are lightweight (e.g. not lights because they - # take up memory) - build_flags |= { - '-DUSE_ADC_SENSOR', - '-DUSE_BINARY_SENSOR', - '-DUSE_DALLAS_SENSOR', - '-DUSE_DHT_SENSOR', - '-DUSE_GPIO_BINARY_SENSOR', - '-DUSE_GPIO_SWITCH', - '-DUSE_SENSOR', - '-DUSE_STATUS_BINARY_SENSOR', - '-DUSE_STATUS_LED', - '-DUSE_SWITCH', - '-DUSE_TEMPLATE_BINARY_SENSOR', - '-DUSE_TEMPLATE_SENSOR', - '-DUSE_TEMPLATE_SWITCH', - '-DUSE_WIFI_SIGNAL_SENSOR', - } - - if CORE.is_esp8266 and CORE.board in ESP8266_FLASH_SIZES and \ - CORE.arduino_version != ARDUINO_VERSION_ESP8266_2_3_0: - flash_size = ESP8266_FLASH_SIZES[CORE.board] - ld_scripts = ESP8266_LD_SCRIPTS[flash_size] - ld_script = None - - if CORE.arduino_version in ('espressif8266@1.8.0', 'espressif8266@1.7.3', - 'espressif8266@1.6.0'): - ld_script = ld_scripts[0] - elif CORE.arduino_version in (ARDUINO_VERSION_ESP8266_DEV, ARDUINO_VERSION_ESP8266_2_5_0): - ld_script = ld_scripts[1] - - if ld_script is not None: - build_flags.add('-Wl,-T{}'.format(ld_script)) - - if CORE.is_esp8266 and CORE.arduino_version in (ARDUINO_VERSION_ESP8266_DEV, - ARDUINO_VERSION_ESP8266_2_5_0): - build_flags.add('-fno-exceptions') + build_flags = CORE.build_flags # avoid changing build flags order return list(sorted(list(build_flags))) @@ -396,31 +214,9 @@ def get_ini_content(): flash_mode = CORE.config[CONF_ESPHOME][CONF_BOARD_FLASH_MODE] data['board_build.flash_mode'] = flash_mode - if not CORE.config[CONF_ESPHOME][CONF_USE_CUSTOM_CODE]: - # Ignore libraries that are not explicitly used, but may - # be added by LDF - data['lib_ldf_mode'] = 'chain' - REMOVABLE_LIBRARIES = [ - 'ArduinoOTA', - 'Update', - 'Wire', - 'FastLED', - 'NeoPixelBus', - 'ESP Async WebServer', - 'AsyncMqttClient', - 'AsyncTCP', - 'ESPAsyncTCP', - ] - ignore = [] - for x in REMOVABLE_LIBRARIES: - for o in lib_deps: - if o.startswith(x): - break - else: - ignore.append(x) - if ignore: - data['lib_ignore'] = ignore - + # Ignore libraries that are not explicitly used, but may + # be added by LDF + # data['lib_ldf_mode'] = 'chain' data.update(CORE.config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS, {})) content = u'[env:{}]\n'.format(CORE.name) @@ -448,65 +244,131 @@ def find_begin_end(text, begin_s, end_s): return text[:begin_index], text[(end_index + len(end_s)):] -def write_platformio_ini(content, path): - symlink_esphome_core_version(CORE.esphome_core_version) - update_esphome_core_repo() +def write_platformio_ini(content): update_storage_json() + path = CORE.relative_build_path('platformio.ini') if os.path.isfile(path): - try: - with codecs.open(path, 'r', encoding='utf-8') as f_handle: - text = f_handle.read() - except OSError: - raise EsphomeError(u"Could not read ini file at {}".format(path)) - prev_file = text + text = read_file(path) content_format = find_begin_end(text, INI_AUTO_GENERATE_BEGIN, INI_AUTO_GENERATE_END) else: - prev_file = None content_format = INI_BASE_FORMAT full_file = content_format[0] + INI_AUTO_GENERATE_BEGIN + '\n' + content full_file += INI_AUTO_GENERATE_END + content_format[1] - if prev_file == full_file: - return - with codecs.open(path, mode='w+', encoding='utf-8') as f_handle: - f_handle.write(full_file) + write_file_if_changed(full_file, path) def write_platformio_project(): mkdir_p(CORE.build_path) - platformio_ini = CORE.relative_build_path('platformio.ini') content = get_ini_content() write_gitignore() - write_platformio_ini(content, platformio_ini) + write_platformio_ini(content) + + +DEFINES_H_FORMAT = u"""\ +#pragma once +{} +""" +DEFINES_H_TARGET = 'esphome/core/defines.h' +ESPHOME_README_TXT = u""" +THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY + +ESPHome automatically populates the esphome/ directory, and any +changes to this directory will be removed the next time esphome is +run. + +For modifying esphome's core files, please use a development esphome install +or use the custom_components folder. +""" + + +def walk_files(path): + for root, _, files in os.walk(path): + for name in files: + yield os.path.join(root, name) + + +def copy_src_tree(): + import filecmp + import shutil + + source_files = {} + for _, component, _ in iter_components(CORE.config): + source_files.update(component.source_files) + + # Convert to list and sort + source_files_l = [it for it in source_files.items()] + source_files_l.sort() + + # Build #include list for main.cpp + include_l = [] + for target, path in source_files_l: + if os.path.splitext(path)[1] in HEADER_FILE_EXTENSIONS: + include_l.append(u'#include "{}"'.format(target)) + include_l.append(u'') + include_s = u'\n'.join(include_l) + + source_files_copy = source_files.copy() + source_files_copy.pop(DEFINES_H_TARGET) + + for path in walk_files(CORE.relative_src_path('esphome')): + if os.path.splitext(path)[1] not in SOURCE_FILE_EXTENSIONS: + # Not a source file, ignore + continue + # Transform path to target path name + target = os.path.relpath(path, CORE.relative_src_path()).replace(os.path.sep, '/') + if target == DEFINES_H_TARGET: + # Ignore defines.h, will be dealt with later + continue + if target not in source_files_copy: + # Source file removed, delete target + os.remove(path) + else: + src_path = source_files_copy.pop(target) + if not filecmp.cmp(path, src_path): + # Files are not same, copy + shutil.copy(src_path, path) + + # Now copy new files + for target, src_path in source_files_copy.items(): + dst_path = CORE.relative_src_path(*target.split('/')) + mkdir_p(os.path.dirname(dst_path)) + shutil.copy(src_path, dst_path) + + # Finally copy defines + write_file_if_changed(generate_defines_h(), + CORE.relative_src_path('esphome', 'core', 'defines.h')) + write_file_if_changed(ESPHOME_README_TXT, + CORE.relative_src_path('esphome', 'README.txt')) + + return include_s + + +def generate_defines_h(): + define_content_l = [x.as_macro for x in CORE.defines] + define_content_l.sort() + return DEFINES_H_FORMAT.format(u'\n'.join(define_content_l)) def write_cpp(code_s): - path = CORE.relative_build_path('src', 'main.cpp') + path = CORE.relative_src_path('main.cpp') if os.path.isfile(path): - try: - with codecs.open(path, 'r', encoding='utf-8') as f_handle: - text = f_handle.read() - except OSError: - raise EsphomeError(u"Could not read C++ file at {}".format(path)) - prev_file = text + text = read_file(path) code_format = find_begin_end(text, CPP_AUTO_GENERATE_BEGIN, CPP_AUTO_GENERATE_END) code_format_ = find_begin_end(code_format[0], CPP_INCLUDE_BEGIN, CPP_INCLUDE_END) code_format = (code_format_[0], code_format_[1], code_format[1]) else: - prev_file = None - mkdir_p(os.path.dirname(path)) code_format = CPP_BASE_FORMAT - include_s = get_include_text() + include_s = copy_src_tree() + global_s = include_s + u'\n' + global_s += CORE.cpp_global_section - full_file = code_format[0] + CPP_INCLUDE_BEGIN + u'\n' + include_s + CPP_INCLUDE_END + full_file = code_format[0] + CPP_INCLUDE_BEGIN + u'\n' + global_s + CPP_INCLUDE_END full_file += code_format[1] + CPP_AUTO_GENERATE_BEGIN + u'\n' + code_s + CPP_AUTO_GENERATE_END full_file += code_format[2] - if prev_file == full_file: - return - with codecs.open(path, 'w+', encoding='utf-8') as f_handle: - f_handle.write(full_file) + write_file_if_changed(full_file, path) def clean_build(): diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 96132e69f..7caa99fbd 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -1,7 +1,6 @@ from __future__ import print_function import codecs -from collections import OrderedDict import fnmatch import logging import os @@ -13,6 +12,7 @@ import yaml.constructor from esphome import core from esphome.core import EsphomeError, HexInt, IPAddress, Lambda, MACAddress, TimePeriod from esphome.py_compat import string_types, text_type, IS_PY2 +from esphome.util import OrderedDict _LOGGER = logging.getLogger(__name__) @@ -397,6 +397,10 @@ yaml.SafeDumper.add_representer( ) yaml.SafeDumper.add_representer(str, unicode_representer) +yaml.SafeDumper.add_representer( + core.AutoLoad, + lambda dumper, value: represent_odict(dumper, 'tag:yaml.org,2002:map', value) +) if IS_PY2: yaml.SafeDumper.add_representer(unicode, unicode_representer) yaml.SafeDumper.add_representer(HexInt, hex_int_representer) diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 000000000..f3ac38ae9 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,48 @@ +; This file is so that the C++ files in this repo +; can be edited with IDEs like VSCode or CLion +; with the platformio system +; It's *not* used during runtime. + +[platformio] +env_default = livingroom8266 +src_dir = . +include_dir = include + +[common] +lib_deps = + AsyncTCP@1.0.3 + AsyncMqttClient@0.8.2 + ArduinoJson-esphomelib@5.13.3 + ESP Async WebServer@1.1.1 + FastLED@3.2.0 + NeoPixelBus@2.4.1 + ESPAsyncTCP@1.2.0 +build_flags = + -Wno-reorder + -DUSE_WEB_SERVER + -DUSE_FAST_LED_LIGHT + -DUSE_NEO_PIXEL_BUS_LIGHT + -DDONT_STORE_LOG_STR_IN_FLASH + -DCLANG_TIDY +; Don't use FlashStringHelper for debug builds because CLion freaks out for all +; log messages +src_filter = + + +[env:livingroom32] +platform = espressif32@1.6.0 +board = nodemcu-32s +framework = arduino +lib_deps = ${common.lib_deps} +build_flags = ${common.build_flags} -DUSE_ETHERNET +src_filter = ${common.src_filter} + + +[env:livingroom8266] +platform = espressif8266@1.8.0 +board = nodemcuv2 +framework = arduino +lib_deps = + ${common.lib_deps} + ESP8266WiFi + Hash +build_flags = ${common.build_flags} -DESPHOME_LOG_LEVEL=6 +src_filter = ${common.src_filter} + diff --git a/requirements.txt b/requirements.txt index 848f03a0c..178d3d445 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,5 +8,6 @@ protobuf>=3.7,<3.8 tzlocal>=1.5.1 pyserial>=3.4,<4 ifaddr>=0.1.6,<1 -platformio>=3.6.5 +platformio>=3.6.5 ; python_version<"3" +https://github.com/platformio/platformio-core/archive/develop.zip ; python_version>"3" esptool>=2.6,<3 diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100644 index 000000000..079b0bf85 --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1,18 @@ +voluptuous>=0.11.5,<0.12 +PyYAML>=5.1,<6 +paho-mqtt>=1.4,<2 +colorlog>=4.0.2 +tornado>=5.1.1,<6 +typing>=3.6.6 ; python_version<"3.5" +protobuf>=3.7,<3.8 +tzlocal>=1.5.1 +pyserial>=3.4,<4 +ifaddr>=0.1.6,<1 +platformio>=3.6.5 ; python_version<"3" +https://github.com/platformio/platformio-core/archive/develop.zip ; python_version>"3" +esptool>=2.6,<3 +pylint==1.9.4 ; python_version<"3" +pylint==2.3.0 ; python_version>"3" +flake8==3.6.0 +pillow +pexpect diff --git a/script/.neopixelbus.patch b/script/.neopixelbus.patch new file mode 100644 index 000000000..8dc2e74b6 --- /dev/null +++ b/script/.neopixelbus.patch @@ -0,0 +1,26 @@ +--- .piolibdeps/NeoPixelBus_ID547/src/internal/NeoEsp8266DmaMethod.h 2018-12-25 06:37:53.000000000 +0100 ++++ .piolibdeps/NeoPixelBus_ID547/src/internal/NeoEsp8266DmaMethod.h.2 2019-03-01 22:18:10.000000000 +0100 +@@ -169,7 +169,7 @@ + _i2sBufDesc[indexDesc].sub_sof = 0; + _i2sBufDesc[indexDesc].datalen = blockSize; + _i2sBufDesc[indexDesc].blocksize = blockSize; +- _i2sBufDesc[indexDesc].buf_ptr = (uint32_t)is2Buffer; ++ _i2sBufDesc[indexDesc].buf_ptr = is2Buffer; + _i2sBufDesc[indexDesc].unused = 0; + _i2sBufDesc[indexDesc].next_link_ptr = (uint32_t)&(_i2sBufDesc[indexDesc + 1]); + +@@ -329,11 +329,13 @@ + case NeoDmaState_Sending: + { + slc_queue_item* finished_item = (slc_queue_item*)SLCRXEDA; ++ uint32_t **ptr = reinterpret_cast(&finished_item); ++ uint32_t dat = *reinterpret_cast(ptr); + + // the data block had actual data sent + // point last state block to first state block thus + // just looping and not sending the data blocks +- (finished_item + 1)->next_link_ptr = (uint32_t)(finished_item); ++ (finished_item + 1)->next_link_ptr = dat; + + s_this->_dmaState = NeoDmaState_Idle; + } diff --git a/script/ci-custom.py b/script/ci-custom.py new file mode 100755 index 000000000..ce69cd945 --- /dev/null +++ b/script/ci-custom.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +from __future__ import print_function + +import codecs +import collections +import os.path +import sys + + +def find_all(a_str, sub): + for i, line in enumerate(a_str.splitlines()): + column = 0 + while True: + column = line.find(sub, column) + if column == -1: + break + yield i, column + column += len(sub) + + +files = [] +for root, _, fs in os.walk('esphome'): + for f in fs: + _, ext = os.path.splitext(f) + if ext in ('.h', '.c', '.cpp', '.tcc', '.py'): + files.append(os.path.join(root, f)) +files.sort() + +errors = collections.defaultdict(list) +for f in files: + try: + with codecs.open(f, 'r', encoding='utf-8') as f_handle: + content = f_handle.read() + except UnicodeDecodeError: + errors[f].append("File is not readable as UTF-8. Please set your editor to UTF-8 mode.") + continue + for line, col in find_all(content, '\t'): + errors[f].append("File contains tab character on line {}:{}. " + "Please convert tabs to spaces.".format(line, col)) + for line, col in find_all(content, '\r'): + errors[f].append("File contains windows newline on line {}:{}. " + "Please set your editor to unix newline mode.".format(line, col)) + if content and not content.endswith('\n'): + errors[f].append("File does not end with a newline, please add an empty line at the end of " + "the file.") + +for f, errs in errors.items(): + print("\033[0;32m************* File \033[1;32m{}\033[0m".format(f)) + for err in errs: + print(err) + print() + +sys.exit(len(errors)) diff --git a/script/ci-suggest-changes b/script/ci-suggest-changes new file mode 100755 index 000000000..b8b441222 --- /dev/null +++ b/script/ci-suggest-changes @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -e + +if git diff-index --quiet HEAD --; then + echo "No changes detected, formatting is correct!" + exit 0 +else + echo "=========================================================" + echo "Your formatting is not correct, ESPHome uses clang-format to format" + echo "all source files in a unified way. Please apply the changes listed below" + echo + echo "The following files need to be changed:" + git diff HEAD --name-only | sed 's/^/ /' + echo + echo + echo "=========================================================" + echo + git diff HEAD + exit 1 +fi diff --git a/script/clang-format.py b/script/clang-format.py new file mode 100755 index 000000000..f928e76e2 --- /dev/null +++ b/script/clang-format.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python + +from __future__ import print_function + +import multiprocessing +import os +import re +import subprocess +import sys + +import argparse +import click +import threading + +is_py2 = sys.version[0] == '2' + +if is_py2: + import Queue as queue +else: + import queue as queue + +root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, '..', '..'))) +basepath = os.path.join(root_path, 'esphome') +rel_basepath = os.path.relpath(basepath, os.getcwd()) + + +def run_format(args, queue, lock): + """Takes filenames out of queue and runs clang-tidy on them.""" + while True: + path = queue.get() + invocation = ['clang-format-7'] + if args.inplace: + invocation.append('-i') + invocation.append(path) + + proc = subprocess.Popen(invocation, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + output, err = proc.communicate() + with lock: + if proc.returncode != 0: + print(' '.join(invocation)) + print(output.decode('utf-8')) + print(err.decode('utf-8')) + queue.task_done() + + +def progress_bar_show(value): + if value is None: + return '' + return value + + +def walk_files(path): + for root, _, files in os.walk(path): + for name in files: + yield os.path.join(root, name) + + +def get_output(*args): + proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output, err = proc.communicate() + return output.decode('utf-8') + + +def splitlines_no_ends(string): + return [s.strip() for s in string.splitlines()] + + +def filter_changed(files): + for remote in ('upstream', 'origin'): + command = ['git', 'merge-base', '{}/dev'.format(remote), 'HEAD'] + try: + merge_base = splitlines_no_ends(get_output(*command))[0] + break + except: + pass + else: + return files + command = ['git', 'diff', merge_base, '--name-only'] + changed = splitlines_no_ends(get_output(*command)) + changed = {os.path.relpath(f, os.getcwd()) for f in changed} + print("Changed Files:") + files = [p for p in files if p in changed] + for p in files: + print(" {}".format(p)) + if not files: + print(" No changed files") + return files + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-j', '--jobs', type=int, + default=multiprocessing.cpu_count(), + help='number of tidy instances to be run in parallel.') + parser.add_argument('files', nargs='*', default=[], + help='files to be processed (regex on path)') + parser.add_argument('-i', '--inplace', action='store_true', + help='apply fix-its') + parser.add_argument('-q', '--quiet', action='store_false', + help='Run clang-tidy in quiet mode') + parser.add_argument('-c', '--changed', action='store_true', + help='Only run on changed files') + args = parser.parse_args() + + try: + get_output('clang-format-7', '-version') + except: + print(""" + Oops. It looks like clang-format is not installed. + + Please check you can run "clang-format-7 -version" in your terminal and install + clang-format (v7) if necessary. + + Note you can also upload your code as a pull request on GitHub and see the CI check + output to apply clang-format. + """) + return 1 + + files = [] + for path in walk_files(basepath): + filetypes = ('.cpp', '.h', '.tcc') + ext = os.path.splitext(path)[1] + if ext in filetypes: + path = os.path.relpath(path, os.getcwd()) + files.append(path) + # Match against re + file_name_re = re.compile('|'.join(args.files)) + files = [p for p in files if file_name_re.search(p)] + + if args.changed: + files = filter_changed(files) + + files.sort() + + return_code = 0 + try: + task_queue = queue.Queue(args.jobs) + lock = threading.Lock() + for _ in range(args.jobs): + t = threading.Thread(target=run_format, + args=(args, task_queue, lock)) + t.daemon = True + t.start() + + # Fill the queue with files. + with click.progressbar(files, width=30, file=sys.stderr, + item_show_func=progress_bar_show) as bar: + for name in bar: + task_queue.put(name) + + # Wait for all threads to be done. + task_queue.join() + + except KeyboardInterrupt: + print() + print('Ctrl-C detected, goodbye.') + os.kill(0, 9) + + sys.exit(return_code) + + +if __name__ == '__main__': + main() diff --git a/script/clang-tidy.py b/script/clang-tidy.py new file mode 100755 index 000000000..1dbc43ca9 --- /dev/null +++ b/script/clang-tidy.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python + +from __future__ import print_function + +import codecs +import json +import multiprocessing +import os +import re + +import pexpect +import shutil +import subprocess +import sys +import tempfile + +import argparse +import click +import threading + +is_py2 = sys.version[0] == '2' + +if is_py2: + import Queue as queue +else: + import queue as queue + +root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, '..', '..'))) +basepath = os.path.join(root_path, 'esphome') +rel_basepath = os.path.relpath(basepath, os.getcwd()) +temp_header_file = os.path.join(root_path, '.temp-clang-tidy.cpp') + + +def run_tidy(args, tmpdir, queue, lock, failed_files): + while True: + path = queue.get() + invocation = ['clang-tidy-7', '-header-filter=^{}/.*'.format(re.escape(basepath))] + if tmpdir is not None: + invocation.append('-export-fixes') + # Get a temporary file. We immediately close the handle so clang-tidy can + # overwrite it. + (handle, name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir) + os.close(handle) + invocation.append(name) + invocation.append('-p=.') + if args.quiet: + invocation.append('-quiet') + invocation.append(os.path.abspath(path)) + invocation_s = ' '.join(shlex_quote(x) for x in invocation) + + # Use pexpect for a pseudy-TTY with colored output + output, rc = pexpect.run(invocation_s, withexitstatus=True, encoding='utf-8', + timeout=15*60) + with lock: + if rc != 0: + print() + print("\033[0;32m************* File \033[1;32m{}\033[0m".format(path)) + print(invocation_s) + print(output) + print() + failed_files.append(path) + queue.task_done() + + +def progress_bar_show(value): + if value is None: + return '' + return value + + +def walk_files(path): + for root, _, files in os.walk(path): + for name in files: + yield os.path.join(root, name) + + +def get_output(*args): + proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output, err = proc.communicate() + return output.decode('utf-8') + + +def splitlines_no_ends(string): + return [s.strip() for s in string.splitlines()] + + +def filter_changed(files): + for remote in ('upstream', 'origin'): + command = ['git', 'merge-base', '{}/dev'.format(remote), 'HEAD'] + try: + merge_base = splitlines_no_ends(get_output(*command))[0] + break + except: + pass + else: + return files + command = ['git', 'diff', merge_base, '--name-only'] + changed = splitlines_no_ends(get_output(*command)) + changed = {os.path.relpath(f, os.getcwd()) for f in changed} + print("Changed Files:") + files = [p for p in files if p in changed] + for p in files: + print(" {}".format(p)) + if not files: + print(" No changed files") + return files + + +def shlex_quote(s): + if not s: + return u"''" + if re.search(r'[^\w@%+=:,./-]', s) is None: + return s + + return u"'" + s.replace(u"'", u"'\"'\"'") + u"'" + + +def build_compile_commands(): + gcc_flags_json = os.path.join(root_path, '.gcc-flags.json') + if not os.path.isfile(gcc_flags_json): + print("Could not find {} file which is required for clang-tidy.") + print('Please run "pio init --ide atom" in the root esphome folder to generate that file.') + sys.exit(1) + with codecs.open(gcc_flags_json, 'r', encoding='utf-8') as f: + gcc_flags = json.load(f) + exec_path = gcc_flags['execPath'] + include_paths = gcc_flags['gccIncludePaths'].split(',') + includes = ['-I{}'.format(p) for p in include_paths] + cpp_flags = gcc_flags['gccDefaultCppFlags'].split(' ') + defines = [flag for flag in cpp_flags if flag.startswith('-D')] + command = [exec_path] + command.extend(includes) + command.extend(defines) + command.append('-std=gnu++11') + + source_files = [] + for path in walk_files(basepath): + filetypes = ('.cpp',) + ext = os.path.splitext(path)[1] + if ext in filetypes: + source_files.append(os.path.abspath(path)) + source_files.append(temp_header_file) + source_files.sort() + compile_commands = [{ + 'directory': root_path, + 'command': ' '.join(shlex_quote(x) for x in (command + ['-o', p + '.o', '-c', p])), + 'file': p + } for p in source_files] + compile_commands_json = os.path.join(root_path, 'compile_commands.json') + if os.path.isfile(compile_commands_json): + with codecs.open(compile_commands_json, 'r', encoding='utf-8') as f: + try: + if json.load(f) == compile_commands: + return + except: + pass + with codecs.open(compile_commands_json, 'w', encoding='utf-8') as f: + json.dump(compile_commands, f, indent=2) + + +def build_all_include(): + # Build a cpp file that includes all header files in this repo. + # Otherwise header-only integrations would not be tested by clang-tidy + headers = [] + for path in walk_files(basepath): + filetypes = ('.h',) + ext = os.path.splitext(path)[1] + if ext in filetypes: + path = os.path.relpath(path, root_path) + include_p = path.replace(os.path.sep, '/') + headers.append('#include "{}"'.format(include_p)) + headers.sort() + headers.append('') + content = '\n'.join(headers) + with codecs.open(temp_header_file, 'w', encoding='utf-8') as f: + f.write(content) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-j', '--jobs', type=int, + default=multiprocessing.cpu_count(), + help='number of tidy instances to be run in parallel.') + parser.add_argument('files', nargs='*', default=[], + help='files to be processed (regex on path)') + parser.add_argument('--fix', action='store_true', help='apply fix-its') + parser.add_argument('-q', '--quiet', action='store_false', + help='Run clang-tidy in quiet mode') + parser.add_argument('-c', '--changed', action='store_true', + help='Only run on changed files') + parser.add_argument('--all-headers', action='store_true', + help='Create a dummy file that checks all headers') + args = parser.parse_args() + + try: + get_output('clang-tidy-7', '-version') + except: + print(""" + Oops. It looks like clang-tidy is not installed. + + Please check you can run "clang-tidy-7 -version" in your terminal and install + clang-tidy (v7) if necessary. + + Note you can also upload your code as a pull request on GitHub and see the CI check + output to apply clang-tidy. + """) + return 1 + + build_compile_commands() + + files = [] + for path in walk_files(basepath): + filetypes = ('.cpp',) + ext = os.path.splitext(path)[1] + if ext in filetypes: + path = os.path.relpath(path, os.getcwd()) + files.append(path) + # Match against re + file_name_re = re.compile('|'.join(args.files)) + files = [p for p in files if file_name_re.search(p)] + + if args.changed: + files = filter_changed(files) + + files.sort() + + if args.all_headers: + build_all_include() + files.insert(0, temp_header_file) + + tmpdir = None + if args.fix: + tmpdir = tempfile.mkdtemp() + + failed_files = [] + return_code = 0 + try: + task_queue = queue.Queue(args.jobs) + lock = threading.Lock() + for _ in range(args.jobs): + t = threading.Thread(target=run_tidy, + args=(args, tmpdir, task_queue, lock, failed_files)) + t.daemon = True + t.start() + + # Fill the queue with files. + with click.progressbar(files, width=30, file=sys.stderr, + item_show_func=progress_bar_show) as bar: + for name in bar: + task_queue.put(name) + + # Wait for all threads to be done. + task_queue.join() + return_code = len(failed_files) + + except KeyboardInterrupt: + print() + print('Ctrl-C detected, goodbye.') + if tmpdir: + shutil.rmtree(tmpdir) + if os.path.exists(temp_header_file): + os.remove(temp_header_file) + os.kill(0, 9) + + if args.fix and failed_files: + print('Applying fixes ...') + try: + subprocess.call(['clang-apply-replacements-7', tmpdir]) + except: + print('Error applying fixes.\n', file=sys.stderr) + if os.path.exists(temp_header_file): + os.remove(temp_header_file) + raise + + if os.path.exists(temp_header_file): + os.remove(temp_header_file) + sys.exit(return_code) + + +if __name__ == '__main__': + main() diff --git a/script/lint-cpp b/script/lint-cpp new file mode 100755 index 000000000..c38351b60 --- /dev/null +++ b/script/lint-cpp @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." +if [[ ! -e ".gcc-flags.json" ]]; then + pio init --ide atom +fi +if ! patch -R -p0 -s -f --dry-run