From 971d121398b165c908bce3c585160c4403097f26 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 18 Apr 2018 10:30:46 +0200 Subject: [PATCH] Preparations for 1.3 --- esphomeyaml/components/deep_sleep.py | 11 ++- esphomeyaml/components/mqtt.py | 34 ++++++-- esphomeyaml/components/output/esp8266_pwm.py | 5 +- esphomeyaml/components/web_server.py | 13 ++- esphomeyaml/components/wifi.py | 86 +++++++++++++++----- esphomeyaml/config.py | 28 +++++-- esphomeyaml/config_validation.py | 4 +- esphomeyaml/const.py | 4 + esphomeyaml/helpers.py | 21 +++-- esphomeyaml/mqtt.py | 20 +++-- esphomeyaml/pins.py | 2 +- esphomeyaml/writer.py | 9 +- 12 files changed, 167 insertions(+), 70 deletions(-) diff --git a/esphomeyaml/components/deep_sleep.py b/esphomeyaml/components/deep_sleep.py index 3ed50ae369..23ca62498f 100644 --- a/esphomeyaml/components/deep_sleep.py +++ b/esphomeyaml/components/deep_sleep.py @@ -1,9 +1,9 @@ import voluptuous as vol from esphomeyaml import config_validation as cv, pins -from esphomeyaml.const import CONF_SLEEP_DURATION, CONF_WAKEUP_PIN, CONF_RUN_CYCLES, \ - CONF_RUN_DURATION, CONF_ID, CONF_NUMBER -from esphomeyaml.helpers import App, add, Pvariable, exp_gpio_input_pin +from esphomeyaml.const import CONF_ID, CONF_RUN_CYCLES, CONF_RUN_DURATION, CONF_SLEEP_DURATION, \ + CONF_WAKEUP_PIN +from esphomeyaml.helpers import App, Pvariable, add, exp_gpio_input_pin DEPENDENCIES = ['logger'] @@ -11,9 +11,8 @@ DEPENDENCIES = ['logger'] def validate_pin_number(value): valid_pins = [0, 2, 4, 12, 13, 14, 15, 25, 26, 27, 32, 39] if value not in valid_pins: - raise vol.Invalid(u"Only pins {} support wakeup".format( - ', '.join(str(x) for x in valid_pins)) - ) + raise vol.Invalid(u"Only pins {} support wakeup" + u"".format(', '.join(str(x) for x in valid_pins))) return value diff --git a/esphomeyaml/components/mqtt.py b/esphomeyaml/components/mqtt.py index 0dc7ecdae8..c81e9f4bb8 100644 --- a/esphomeyaml/components/mqtt.py +++ b/esphomeyaml/components/mqtt.py @@ -1,11 +1,14 @@ +import re + import voluptuous as vol import esphomeyaml.config_validation as cv -from esphomeyaml.const import CONF_BIRTH_MESSAGE, CONF_BROKER, CONF_DISCOVERY, \ - CONF_DISCOVERY_PREFIX, CONF_DISCOVERY_RETAIN, CONF_ID, CONF_MQTT, CONF_PASSWORD, \ - CONF_PAYLOAD, CONF_PORT, CONF_QOS, CONF_RETAIN, CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_USERNAME, \ - CONF_WILL_MESSAGE, CONF_CLIENT_ID, CONF_LOG_TOPIC -from esphomeyaml.helpers import App, Pvariable, StructInitializer, add, exp_empty_optional +from esphomeyaml.const import CONF_BIRTH_MESSAGE, CONF_BROKER, CONF_CLIENT_ID, CONF_DISCOVERY, \ + CONF_DISCOVERY_PREFIX, CONF_DISCOVERY_RETAIN, CONF_FINGERPRINTS, CONF_ID, CONF_LOG_TOPIC, \ + CONF_MQTT, CONF_PASSWORD, CONF_PAYLOAD, CONF_PORT, CONF_QOS, CONF_RETAIN, CONF_TOPIC, \ + CONF_TOPIC_PREFIX, CONF_USERNAME, CONF_WILL_MESSAGE +from esphomeyaml.helpers import App, ArrayInitializer, Pvariable, StructInitializer, add, \ + exp_empty_optional MQTT_WILL_BIRTH_SCHEMA = vol.Any(None, vol.Schema({ vol.Required(CONF_TOPIC): cv.publish_topic, @@ -19,7 +22,7 @@ def validate_broker(value): value = cv.string_strict(value) if value.endswith(u'.local'): raise vol.Invalid(u"MQTT server addresses ending with '.local' are currently unsupported." - u" Please specify the static IP instead.") + u" Please use the static IP instead.") if u':' in value: raise vol.Invalid(u"Please specify the port using the port: option") if not value: @@ -27,6 +30,13 @@ def validate_broker(value): return value +def validate_fingerprint(value): + value = cv.string(value) + if re.match(r'^[0-9a-f]{40}$', value) is None: + raise vol.Invalid(u"fingerprint must be valid SHA1 hash") + return value + + CONFIG_SCHEMA = vol.Schema({ cv.GenerateID(CONF_MQTT): cv.register_variable_id, vol.Required(CONF_BROKER): validate_broker, @@ -41,6 +51,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, vol.Optional(CONF_TOPIC_PREFIX): cv.publish_topic, vol.Optional(CONF_LOG_TOPIC): cv.publish_topic, + vol.Optional(CONF_FINGERPRINTS): vol.All(cv.only_on_esp8266, + cv.ensure_list, [validate_fingerprint]), }) @@ -77,3 +89,13 @@ def to_code(config): add(mqtt.set_client_id(config[CONF_CLIENT_ID])) if CONF_LOG_TOPIC in config: add(mqtt.set_log_topic(config[CONF_LOG_TOPIC])) + if CONF_FINGERPRINTS in config: + for fingerprint in config[CONF_FINGERPRINTS]: + arr = [fingerprint[i:i + 2] for i in range(0, 40, 2)] + add(mqtt.add_ssl_fingerprint(ArrayInitializer(*arr))) + + +def build_flags(config): + if CONF_FINGERPRINTS in config: + return '-DASYNC_TCP_SSL_ENABLED' + return None diff --git a/esphomeyaml/components/output/esp8266_pwm.py b/esphomeyaml/components/output/esp8266_pwm.py index dfe907a78b..4d299d4e62 100644 --- a/esphomeyaml/components/output/esp8266_pwm.py +++ b/esphomeyaml/components/output/esp8266_pwm.py @@ -2,10 +2,9 @@ import voluptuous as vol from esphomeyaml import pins from esphomeyaml.components import output -from esphomeyaml.const import CONF_ID, CONF_PIN, \ - ESP_PLATFORM_ESP8266 +from esphomeyaml.const import CONF_ID, CONF_PIN, ESP_PLATFORM_ESP8266 from esphomeyaml.core import ESPHomeYAMLError -from esphomeyaml.helpers import App, Pvariable, exp_gpio_output_pin, get_gpio_pin_number +from esphomeyaml.helpers import App, Pvariable, exp_gpio_output_pin ESP_PLATFORMS = [ESP_PLATFORM_ESP8266] diff --git a/esphomeyaml/components/web_server.py b/esphomeyaml/components/web_server.py index 6811c76a91..3eff1b8160 100644 --- a/esphomeyaml/components/web_server.py +++ b/esphomeyaml/components/web_server.py @@ -3,19 +3,26 @@ import logging import voluptuous as vol import esphomeyaml.config_validation as cv -from esphomeyaml.const import CONF_PORT -from esphomeyaml.helpers import App, add +from esphomeyaml.const import CONF_PORT, CONF_JS_URL, CONF_CSS_URL, CONF_ID +from esphomeyaml.helpers import App, add, Pvariable _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({ cv.GenerateID('web_server'): cv.register_variable_id, vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_CSS_URL): vol.Url, + vol.Optional(CONF_JS_URL): vol.Url, }) def to_code(config): - add(App.init_web_server(config.get(CONF_PORT))) + rhs = App.init_web_server(config.get(CONF_PORT)) + web_server = Pvariable('WebServer', config[CONF_ID], rhs) + if CONF_CSS_URL in config: + add(web_server.set_css_url(config[CONF_CSS_URL])) + if CONF_JS_URL in config: + add(web_server.set_js_url(config[CONF_JS_URL])) def build_flags(config): diff --git a/esphomeyaml/components/wifi.py b/esphomeyaml/components/wifi.py index 3f4c9d8630..ed1d2bf1da 100644 --- a/esphomeyaml/components/wifi.py +++ b/esphomeyaml/components/wifi.py @@ -2,24 +2,47 @@ import voluptuous as vol import esphomeyaml.config_validation as cv from esphomeyaml.const import CONF_DNS1, CONF_DNS2, CONF_GATEWAY, CONF_HOSTNAME, CONF_ID, \ - CONF_MANUAL_IP, CONF_PASSWORD, CONF_SSID, CONF_STATIC_IP, CONF_SUBNET, CONF_WIFI + CONF_MANUAL_IP, CONF_PASSWORD, CONF_SSID, CONF_STATIC_IP, CONF_SUBNET, CONF_WIFI, CONF_AP, \ + CONF_CHANNEL from esphomeyaml.helpers import App, MockObj, Pvariable, StructInitializer, add + +def validate_password(value): + value = cv.string(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) > 63: + raise vol.Invalid(u"WPA password must be at most 63 characters long") + return value + + +AP_MANUAL_IP_SCHEMA = vol.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.Inclusive(CONF_DNS1, 'dns'): cv.ipv4, + vol.Inclusive(CONF_DNS2, 'dns'): cv.ipv4, +}) + CONFIG_SCHEMA = vol.Schema({ cv.GenerateID(CONF_WIFI): cv.register_variable_id, - vol.Required(CONF_SSID): cv.ssid, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_MANUAL_IP): vol.Schema({ - vol.Required(CONF_STATIC_IP): cv.ipv4, - vol.Required(CONF_GATEWAY): cv.ipv4, - vol.Required(CONF_SUBNET): cv.ipv4, - vol.Inclusive(CONF_DNS1, 'dns'): cv.ipv4, - vol.Inclusive(CONF_DNS2, 'dns'): cv.ipv4, + 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): vol.Schema({ + vol.Required(CONF_SSID): cv.ssid, + vol.Optional(CONF_PASSWORD): validate_password, + vol.Optional(CONF_CHANNEL): vol.All(cv.positive_int, vol.Range(min=1, max=14)), + vol.Optional(CONF_MANUAL_IP): AP_MANUAL_IP_SCHEMA, }), vol.Optional(CONF_HOSTNAME): cv.hostname, }) - # pylint: disable=invalid-name IPAddress = MockObj('IPAddress') @@ -30,19 +53,38 @@ def safe_ip(ip): return IPAddress(*ip.args) +def manual_ip(config): + 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 to_code(config): - rhs = App.init_wifi(config[CONF_SSID], config.get(CONF_PASSWORD)) + sta = CONF_SSID in config + ap = CONF_AP in config + if sta: + rhs = App.init_wifi(config[CONF_SSID], config.get(CONF_PASSWORD)) + else: + rhs = App.init_wifi() wifi = Pvariable('WiFiComponent', config[CONF_ID], rhs) - if CONF_MANUAL_IP in config: - manual_ip = config[CONF_MANUAL_IP] - exp = StructInitializer( - 'ManualIP', - ('static_ip', safe_ip(manual_ip[CONF_STATIC_IP])), - ('gateway', safe_ip(manual_ip[CONF_GATEWAY])), - ('subnet', safe_ip(manual_ip[CONF_SUBNET])), - ('dns1', safe_ip(manual_ip.get(CONF_DNS1))), - ('dns2', safe_ip(manual_ip.get(CONF_DNS2))), - ) - add(wifi.set_manual_ip(exp)) + + if sta and CONF_MANUAL_IP in config: + add(wifi.set_sta_manual_ip(manual_ip(config[CONF_MANUAL_IP]))) + + if ap: + conf = config[CONF_AP] + password = config.get(CONF_PASSWORD) + if password is None and CONF_CHANNEL in conf: + password = u"" + add(wifi.set_ap(conf[CONF_SSID], password, conf.get(CONF_CHANNEL))) + + if CONF_MANUAL_IP in conf: + add(wifi.set_ap_manual_ip(manual_ip(conf[CONF_MANUAL_IP]))) + if CONF_HOSTNAME in config: add(wifi.set_hostname(config[CONF_HOSTNAME])) diff --git a/esphomeyaml/config.py b/esphomeyaml/config.py index 42d11688ca..d3617f51a9 100644 --- a/esphomeyaml/config.py +++ b/esphomeyaml/config.py @@ -9,10 +9,9 @@ from voluptuous.humanize import humanize_error import esphomeyaml.config_validation as cv from esphomeyaml import core, yaml_util -from esphomeyaml.const import CONF_BOARD, CONF_ESPHOMEYAML, CONF_LIBRARY_URI, CONF_MQTT, \ - CONF_NAME, \ - CONF_PLATFORM, CONF_SIMPLIFY, CONF_WIFI, ESP_PLATFORMS, ESP_PLATFORM_ESP32, \ - ESP_PLATFORM_ESP8266, CONF_USE_BUILD_FLAGS +from esphomeyaml.const import CONF_BOARD, CONF_ESPHOMEYAML, CONF_LIBRARY_URI, CONF_NAME, \ + CONF_PLATFORM, CONF_SIMPLIFY, CONF_USE_BUILD_FLAGS, CONF_WIFI, ESP_PLATFORMS, \ + ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266 from esphomeyaml.core import ESPHomeYAMLError from esphomeyaml.helpers import App, add, color @@ -22,8 +21,7 @@ DEFAULT_LIBRARY_URI = u'esphomelib@1.2.1' CORE_SCHEMA = vol.Schema({ vol.Required(CONF_NAME): cv.valid_name, - vol.Required(CONF_PLATFORM): vol.All( - vol.Upper, vol.Any(ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266)), + vol.Required(CONF_PLATFORM): cv.string, vol.Required(CONF_BOARD): cv.string, vol.Optional(CONF_LIBRARY_URI, default=DEFAULT_LIBRARY_URI): cv.string, vol.Optional(CONF_SIMPLIFY, default=True): cv.boolean, @@ -31,7 +29,7 @@ CORE_SCHEMA = vol.Schema({ }) REQUIRED_COMPONENTS = [ - CONF_ESPHOMEYAML, CONF_WIFI, CONF_MQTT + CONF_ESPHOMEYAML, CONF_WIFI ] _COMPONENT_CACHE = {} @@ -159,6 +157,16 @@ def validate_config(config): result.add_error(u"Platform not found: {}.{}") continue + success = True + dependencies = getattr(platform, 'DEPENDENCIES', []) + for dependency in dependencies: + if dependency not in _ALL_COMPONENTS: + result.add_error(u"Platform {}.{} requires {}".format(domain, p_name, + dependency)) + success = False + if not success: + continue + if hasattr(platform, u'PLATFORM_SCHEMA'): try: p_validated = platform.PLATFORM_SCHEMA(p_config) @@ -201,8 +209,10 @@ def load_config(path): core_conf = config[CONF_ESPHOMEYAML] esp_platform = unicode(core_conf.get(CONF_PLATFORM, u"")) esp_platform = esp_platform.upper() - if esp_platform not in (ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266): - raise ESPHomeYAMLError(u"Invalid ESP Platform {}".format(esp_platform)) + if '8266' in esp_platform: + esp_platform = ESP_PLATFORM_ESP8266 + if '32' in esp_platform: + esp_platform = ESP_PLATFORM_ESP32 core.ESP_PLATFORM = esp_platform core.BOARD = unicode(core_conf.get(CONF_BOARD, u"")) core.SIMPLIFY = cv.boolean(core_conf.get(CONF_SIMPLIFY, True)) diff --git a/esphomeyaml/config_validation.py b/esphomeyaml/config_validation.py index 132c32891c..8b9f4fd9fc 100644 --- a/esphomeyaml/config_validation.py +++ b/esphomeyaml/config_validation.py @@ -272,8 +272,8 @@ def ssid(value): raise vol.Invalid("SSID must be a string. Did you wrap it in quotes?") if not value: raise vol.Invalid("SSID can't be empty.") - if len(value) > 32: - raise vol.Invalid("SSID can't be longer than 32 characters") + if len(value) > 31: + raise vol.Invalid("SSID can't be longer than 31 characters") return value diff --git a/esphomeyaml/const.py b/esphomeyaml/const.py index 908c4f7ffa..773fc99464 100644 --- a/esphomeyaml/const.py +++ b/esphomeyaml/const.py @@ -157,6 +157,10 @@ 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_FINGERPRINTS = 'fingerprints' ESP32_BOARDS = [ 'featheresp32', 'node32s', 'espea32', 'firebeetle32', 'esp32doit-devkit-v1', diff --git a/esphomeyaml/helpers.py b/esphomeyaml/helpers.py index f33b09dd62..dc8c993142 100644 --- a/esphomeyaml/helpers.py +++ b/esphomeyaml/helpers.py @@ -67,6 +67,7 @@ class RawExpression(Expression): return self.text +# pylint: disable=redefined-builtin class AssignmentExpression(Expression): def __init__(self, type, modifier, name, rhs, obj): super(AssignmentExpression, self).__init__() @@ -136,23 +137,27 @@ class StructInitializer(Expression): class ArrayInitializer(Expression): - def __init__(self, *args): + def __init__(self, *args, **kwargs): super(ArrayInitializer, self).__init__() + self.multiline = kwargs.get('multiline', True) self.args = [] - for x in args: - if x is None: + for arg in args: + if arg is None: continue - exp = safe_exp(x) + exp = safe_exp(arg) self.args.append(exp) self.requires.append(exp) def __str__(self): if not self.args: return u'{}' - cpp = u'{\n' - for arg in self.args: - cpp += u' {},\n'.format(arg) - cpp += u'}' + if self.multiline: + cpp = u'{\n' + for arg in self.args: + cpp += u' {},\n'.format(arg) + cpp += u'}' + else: + cpp = u'{' + u', '.join(str(arg) for arg in self.args) + u'}' return cpp diff --git a/esphomeyaml/mqtt.py b/esphomeyaml/mqtt.py index d231df827f..9db005e5af 100644 --- a/esphomeyaml/mqtt.py +++ b/esphomeyaml/mqtt.py @@ -7,8 +7,9 @@ from datetime import datetime import paho.mqtt.client as mqtt from esphomeyaml import core -from esphomeyaml.const import CONF_BROKER, CONF_DISCOVERY_PREFIX, CONF_ESPHOMEYAML, CONF_LOGGER, \ - CONF_LOG_TOPIC, CONF_MQTT, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_TOPIC_PREFIX, \ +from esphomeyaml.const import CONF_BROKER, CONF_DISCOVERY_PREFIX, CONF_ESPHOMEYAML, \ + CONF_LOG_TOPIC, \ + CONF_MQTT, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_TOPIC_PREFIX, \ CONF_USERNAME from esphomeyaml.helpers import color @@ -41,12 +42,17 @@ def initialize(config, subscriptions, on_message, username, password, client_id) def show_logs(config, topic=None, username=None, password=None, client_id=None): if topic is not None: pass # already have topic - elif CONF_LOG_TOPIC in config.get(CONF_MQTT, {}): - topic = config[CONF_MQTT][CONF_LOG_TOPIC] - elif CONF_TOPIC_PREFIX in config[CONF_MQTT]: - topic = config[CONF_MQTT][CONF_TOPIC_PREFIX] + u'/debug' + elif CONF_MQTT in config: + conf = config[CONF_MQTT] + if CONF_LOG_TOPIC in conf: + topic = config[CONF_MQTT][CONF_LOG_TOPIC] + elif CONF_TOPIC_PREFIX in config[CONF_MQTT]: + topic = config[CONF_MQTT][CONF_TOPIC_PREFIX] + u'/debug' + else: + topic = config[CONF_ESPHOMEYAML][CONF_NAME] + u'/debug' else: - topic = config[CONF_ESPHOMEYAML][CONF_NAME] + u'/debug' + _LOGGER.error(u"MQTT isn't setup, can't start MQTT logs") + return 1 _LOGGER.info(u"Starting log output from %s", topic) def on_message(client, userdata, msg): diff --git a/esphomeyaml/pins.py b/esphomeyaml/pins.py index 4605c6b6e6..84c8fbe51f 100644 --- a/esphomeyaml/pins.py +++ b/esphomeyaml/pins.py @@ -206,4 +206,4 @@ def schema_validate_number(validator): value = validator(value) return value - return valid \ No newline at end of file + return valid diff --git a/esphomeyaml/writer.py b/esphomeyaml/writer.py index 8f3c7f516f..7f4dbb30f1 100644 --- a/esphomeyaml/writer.py +++ b/esphomeyaml/writer.py @@ -6,7 +6,7 @@ import os from esphomeyaml.config import iter_components from esphomeyaml.const import CONF_BOARD, CONF_ESPHOMEYAML, CONF_LIBRARY_URI, CONF_NAME, \ - CONF_PLATFORM, ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266, CONF_USE_BUILD_FLAGS + CONF_PLATFORM, CONF_USE_BUILD_FLAGS, ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266 from esphomeyaml.core import ESPHomeYAMLError CPP_AUTO_GENERATE_BEGIN = u'// ========== AUTO GENERATED CODE BEGIN ===========' @@ -64,9 +64,12 @@ PLATFORM_TO_PLATFORMIO = { def get_ini_content(config): + platform = config[CONF_ESPHOMEYAML][CONF_PLATFORM] + if platform in PLATFORM_TO_PLATFORMIO: + platform = PLATFORM_TO_PLATFORMIO[platform] options = { u'env': config[CONF_ESPHOMEYAML][CONF_NAME], - u'platform': PLATFORM_TO_PLATFORMIO[config[CONF_ESPHOMEYAML][CONF_PLATFORM]], + u'platform': platform, u'board': config[CONF_ESPHOMEYAML][CONF_BOARD], u'esphomeyaml_uri': config[CONF_ESPHOMEYAML][CONF_LIBRARY_URI], u'build_flags': u'', @@ -74,7 +77,7 @@ def get_ini_content(config): if config[CONF_ESPHOMEYAML][CONF_USE_BUILD_FLAGS]: build_flags = set() build_flags.add(u"-DESPHOMEYAML_USE") - for domain, component, conf in iter_components(config): + for _, component, conf in iter_components(config): if not hasattr(component, u'build_flags'): continue flags = component.build_flags(conf)