diff --git a/.travis.yml b/.travis.yml index 2b5f0970a2..426bb9a6ed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ python: - "2.7" install: - pip install -r requirements.txt - - pip install tornado esptool flake8==3.5.0 pylint==1.8.4 + - pip install tornado esptool flake8==3.5.0 pylint==1.8.4 tzlocal pillow script: - flake8 esphomeyaml - pylint esphomeyaml diff --git a/esphomeyaml/__main__.py b/esphomeyaml/__main__.py index 93719fcd5c..d4de99fd8a 100644 --- a/esphomeyaml/__main__.py +++ b/esphomeyaml/__main__.py @@ -7,11 +7,11 @@ import random import sys from datetime import datetime -from esphomeyaml import const, core, mqtt, wizard, writer, yaml_util -from esphomeyaml.config import core_to_code, get_component, iter_components, read_config +from esphomeyaml import const, core, core_config, mqtt, wizard, writer, yaml_util +from esphomeyaml.config import get_component, iter_components, read_config from esphomeyaml.const import CONF_BAUD_RATE, CONF_BUILD_PATH, CONF_DOMAIN, CONF_ESPHOMEYAML, \ - CONF_HOSTNAME, CONF_LOGGER, CONF_MANUAL_IP, CONF_NAME, CONF_STATIC_IP, CONF_WIFI, \ - ESP_PLATFORM_ESP8266 + CONF_HOSTNAME, CONF_LOGGER, CONF_MANUAL_IP, CONF_NAME, CONF_STATIC_IP, CONF_USE_CUSTOM_CODE, \ + CONF_WIFI, ESP_PLATFORM_ESP8266 from esphomeyaml.core import ESPHomeYAMLError from esphomeyaml.helpers import AssignmentExpression, Expression, RawStatement, _EXPRESSIONS, add, \ add_job, color, flush_tasks, indent, quote, statement @@ -123,7 +123,7 @@ def run_miniterm(config, port, escape=False): def write_cpp(config): _LOGGER.info("Generating C++ source...") - add_job(core_to_code, config[CONF_ESPHOMEYAML], domain='esphomeyaml') + add_job(core_config.to_code, config[CONF_ESPHOMEYAML], domain='esphomeyaml') for domain in PRE_INITIALIZE: if domain == CONF_ESPHOMEYAML or domain not in config: continue @@ -139,7 +139,7 @@ def write_cpp(config): add(RawStatement('')) all_code = [] for exp in _EXPRESSIONS: - if core.SIMPLIFY: + if not config[CONF_ESPHOMEYAML][CONF_USE_CUSTOM_CODE]: if isinstance(exp, Expression) and not exp.required: continue if isinstance(exp, AssignmentExpression) and not exp.obj.required: @@ -302,7 +302,7 @@ def command_compile(args, config): return exit_code if args.only_generate: _LOGGER.info(u"Successfully generated source code.") - return 0; + return 0 exit_code = compile_program(args, config) if exit_code != 0: return exit_code @@ -388,10 +388,11 @@ def parse_args(argv): subparsers.required = True subparsers.add_parser('config', help='Validate the configuration and spit it out.') - parser_compile = subparsers.add_parser('compile', help='Read the configuration and compile a program.') + parser_compile = subparsers.add_parser('compile', + help='Read the configuration and compile a program.') parser_compile.add_argument('--only-generate', - help="Only generate source code, do not compile.", - action='store_true') + help="Only generate source code, do not compile.", + action='store_true') parser_upload = subparsers.add_parser('upload', help='Validate the configuration ' 'and upload the latest binary.') diff --git a/esphomeyaml/components/binary_sensor/__init__.py b/esphomeyaml/components/binary_sensor/__init__.py index 89f64a554e..f83416d85b 100644 --- a/esphomeyaml/components/binary_sensor/__init__.py +++ b/esphomeyaml/components/binary_sensor/__init__.py @@ -5,7 +5,7 @@ from esphomeyaml import automation from esphomeyaml.const import CONF_DEVICE_CLASS, CONF_ID, CONF_INTERNAL, CONF_INVERTED, \ CONF_MAX_LENGTH, CONF_MIN_LENGTH, CONF_MQTT_ID, CONF_ON_CLICK, CONF_ON_DOUBLE_CLICK, \ CONF_ON_PRESS, CONF_ON_RELEASE, CONF_TRIGGER_ID, CONF_FILTERS, CONF_INVERT, CONF_DELAYED_ON, \ - CONF_DELAYED_OFF, CONF_LAMBDA + CONF_DELAYED_OFF, CONF_LAMBDA, CONF_HEARTBEAT from esphomeyaml.helpers import App, NoArg, Pvariable, add, add_job, esphomelib_ns, \ setup_mqtt_component, bool_, process_lambda, ArrayInitializer @@ -30,6 +30,7 @@ InvertFilter = binary_sensor_ns.InvertFilter LambdaFilter = binary_sensor_ns.LambdaFilter DelayedOnFilter = binary_sensor_ns.DelayedOnFilter DelayedOffFilter = binary_sensor_ns.DelayedOffFilter +HeartbeatFilter = binary_sensor_ns.HeartbeatFilter MQTTBinarySensorComponent = binary_sensor_ns.MQTTBinarySensorComponent FILTER_KEYS = [CONF_INVERT, CONF_DELAYED_ON, CONF_DELAYED_OFF, CONF_LAMBDA] @@ -38,6 +39,7 @@ FILTERS_SCHEMA = vol.All(cv.ensure_list, [vol.All({ vol.Optional(CONF_INVERT): None, vol.Optional(CONF_DELAYED_ON): cv.positive_time_period_milliseconds, vol.Optional(CONF_DELAYED_OFF): cv.positive_time_period_milliseconds, + vol.Optional(CONF_HEARTBEAT): cv.positive_time_period_milliseconds, vol.Optional(CONF_LAMBDA): cv.lambda_, }, cv.has_exactly_one_key(*FILTER_KEYS))]) @@ -82,6 +84,8 @@ def setup_filter(config): yield App.register_component(DelayedOffFilter.new(config[CONF_DELAYED_OFF])) elif CONF_DELAYED_ON in config: yield App.register_component(DelayedOnFilter.new(config[CONF_DELAYED_ON])) + elif CONF_HEARTBEAT in config: + yield App.register_component(HeartbeatFilter.new(config[CONF_HEARTBEAT])) elif CONF_LAMBDA in config: lambda_ = None for lambda_ in process_lambda(config[CONF_LAMBDA], [(bool_, 'x')]): diff --git a/esphomeyaml/components/font.py b/esphomeyaml/components/font.py index a40a3113e5..f18336a0a0 100644 --- a/esphomeyaml/components/font.py +++ b/esphomeyaml/components/font.py @@ -1,6 +1,4 @@ # coding=utf-8 -import os.path - import voluptuous as vol import esphomeyaml.config_validation as cv @@ -8,7 +6,8 @@ from esphomeyaml import core from esphomeyaml.components import display from esphomeyaml.const import CONF_FILE, CONF_GLYPHS, CONF_ID, CONF_SIZE from esphomeyaml.core import HexInt -from esphomeyaml.helpers import App, ArrayInitializer, MockObj, Pvariable, RawExpression, add +from esphomeyaml.helpers import App, ArrayInitializer, MockObj, Pvariable, RawExpression, add, \ + relative_path DEPENDENCIES = ['display'] @@ -57,17 +56,13 @@ def validate_pillow_installed(value): def validate_truetype_file(value): - value = cv.string(value) - path = os.path.join(os.path.dirname(core.CONFIG_PATH), value) - if not os.path.isfile(path): - raise vol.Invalid(u"Could not find file '{}'. Please make sure it exists.".format(path)) if value.endswith('.zip'): # for Google Fonts downloads raise vol.Invalid(u"Please unzip the font archive '{}' first and then use the .ttf files " u"inside.".format(value)) if not value.endswith('.ttf'): raise vol.Invalid(u"Only truetype (.ttf) files are supported. Please make sure you're " u"using the correct format or rename the extension to .ttf") - return value + return cv.file_(value) DEFAULT_GLYPHS = u' !"%()+,-.:0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' @@ -88,7 +83,7 @@ def to_code(config): from PIL import ImageFont for conf in config: - path = os.path.join(os.path.dirname(core.CONFIG_PATH), conf[CONF_FILE]) + path = relative_path(conf[CONF_FILE]) try: font = ImageFont.truetype(path, conf[CONF_SIZE]) except Exception as e: diff --git a/esphomeyaml/components/image.py b/esphomeyaml/components/image.py index 6fd8dbedaa..acb99bbb19 100644 --- a/esphomeyaml/components/image.py +++ b/esphomeyaml/components/image.py @@ -1,6 +1,5 @@ # coding=utf-8 import logging -import os.path import voluptuous as vol @@ -9,7 +8,8 @@ from esphomeyaml import core from esphomeyaml.components import display, font from esphomeyaml.const import CONF_FILE, CONF_ID, CONF_RESIZE from esphomeyaml.core import HexInt -from esphomeyaml.helpers import App, ArrayInitializer, MockObj, Pvariable, RawExpression, add +from esphomeyaml.helpers import App, ArrayInitializer, MockObj, Pvariable, RawExpression, add, \ + relative_path _LOGGER = logging.getLogger(__name__) @@ -17,20 +17,11 @@ DEPENDENCIES = ['display'] Image_ = display.display_ns.Image - -def validate_image_file(value): - value = cv.string(value) - path = os.path.join(os.path.dirname(core.CONFIG_PATH), value) - if not os.path.isfile(path): - raise vol.Invalid(u"Could not find file '{}'. Please make sure it exists.".format(path)) - return value - - CONF_RAW_DATA_ID = 'raw_data_id' IMAGE_SCHEMA = vol.Schema({ vol.Required(CONF_ID): cv.declare_variable_id(Image_), - vol.Required(CONF_FILE): validate_image_file, + vol.Required(CONF_FILE): cv.file_, vol.Optional(CONF_RESIZE): cv.dimensions, cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_variable_id(None), }) @@ -42,7 +33,7 @@ def to_code(config): from PIL import Image for conf in config: - path = os.path.join(os.path.dirname(core.CONFIG_PATH), conf[CONF_FILE]) + path = relative_path(conf[CONF_FILE]) try: image = Image.open(path) except Exception as e: diff --git a/esphomeyaml/components/time/__init__.py b/esphomeyaml/components/time/__init__.py index 007b38ed0c..89a14ea359 100644 --- a/esphomeyaml/components/time/__init__.py +++ b/esphomeyaml/components/time/__init__.py @@ -5,9 +5,10 @@ import math import voluptuous as vol import esphomeyaml.config_validation as cv -from esphomeyaml.const import CONF_TIMEZONE -from esphomeyaml.helpers import add, add_job, esphomelib_ns - +from esphomeyaml import automation +from esphomeyaml.const import CONF_CRON, CONF_DAYS_OF_MONTH, CONF_DAYS_OF_WEEK, CONF_HOURS, \ + CONF_MINUTES, CONF_MONTHS, CONF_ON_TIME, CONF_SECONDS, CONF_TIMEZONE, CONF_TRIGGER_ID +from esphomeyaml.helpers import App, NoArg, Pvariable, add, add_job, esphomelib_ns _LOGGER = logging.getLogger(__name__) @@ -16,6 +17,7 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ }) time_ns = esphomelib_ns.namespace('time') +CronTrigger = time_ns.CronTrigger def _tz_timedelta(td): @@ -117,14 +119,155 @@ def detect_tz(): return tzbase + tzext +def _parse_cron_int(value, special_mapping, message): + special_mapping = special_mapping or {} + if isinstance(value, (str, unicode)) and value in special_mapping: + return special_mapping[value] + try: + return int(value) + except ValueError: + raise vol.Invalid(message.format(value)) + + +def _parse_cron_part(part, min_value, max_value, special_mapping): + if part == '*' or part == '?': + return set(x for x in range(min_value, max_value + 1)) + if '/' in part: + data = part.split('/') + if len(data) > 2: + raise vol.Invalid(u"Can't have more than two '/' in one time expression, got {}" + .format(part)) + offset, repeat = data + offset_n = 0 + if offset: + offset_n = _parse_cron_int(offset, special_mapping, + u"Offset for '/' time expression must be an integer, got {}") + + try: + repeat_n = int(repeat) + except ValueError: + raise vol.Invalid(u"Repeat for '/' time expression must be an integer, got {}" + .format(repeat)) + return set(x for x in range(offset_n, max_value + 1, repeat_n)) + if '-' in part: + data = part.split('-') + if len(data) > 2: + raise vol.Invalid(u"Can't have more than two '-' in range time expression '{}'" + .format(part)) + begin, end = data + begin_n = _parse_cron_int(begin, special_mapping, u"Number for time range must be integer, " + u"got {}") + end_n = _parse_cron_int(end, special_mapping, u"Number for time range must be integer, " + u"got {}") + if end_n < begin_n: + return set(x for x in range(end_n, max_value + 1)) | \ + set(x for x in range(min_value, begin_n + 1)) + return set(x for x in range(begin_n, end_n + 1)) + + return {_parse_cron_int(part, special_mapping, u"Number for time expression must be an " + u"integer, got {}")} + + +def cron_expression_validator(name, min_value, max_value, special_mapping=None): + def validator(value): + if isinstance(value, list): + for v in value: + if not isinstance(v, int): + raise vol.Invalid( + "Expected integer for {} '{}', got {}".format(v, name, type(v))) + if v < min_value or v > max_value: + raise vol.Invalid( + "{} {} is out of range (min={} max={}).".format(name, v, min_value, + max_value)) + return list(sorted(value)) + value = cv.string(value) + values = set() + for part in value.split(','): + values |= _parse_cron_part(part, min_value, max_value, special_mapping) + return validator(list(values)) + + return validator + + +validate_cron_seconds = cron_expression_validator('seconds', 0, 60) +validate_cron_minutes = cron_expression_validator('minutes', 0, 59) +validate_cron_hours = cron_expression_validator('hours', 0, 23) +validate_cron_days_of_month = cron_expression_validator('days of month', 1, 31) +validate_cron_months = cron_expression_validator('months', 1, 12, { + 'JAN': 1, 'FEB': 2, 'MAR': 3, 'APR': 4, 'MAY': 5, 'JUN': 6, 'JUL': 7, 'AUG': 8, + 'SEP': 9, 'OCT': 10, 'NOV': 11, 'DEC': 12 +}) +validate_cron_days_of_week = cron_expression_validator('days of week', 1, 7, { + 'SUN': 1, 'MON': 2, 'TUE': 3, 'WED': 4, 'THU': 5, 'FRI': 6, 'SAT': 7 +}) +CRON_KEYS = [CONF_SECONDS, CONF_MINUTES, CONF_HOURS, CONF_DAYS_OF_MONTH, CONF_MONTHS, + CONF_DAYS_OF_WEEK] + + +def validate_cron_raw(value): + value = cv.string(value) + value = value.split(' ') + if len(value) != 6: + raise vol.Invalid("Cron expression must consist of exactly 6 space-separated parts, " + "not {}".format(len(value))) + seconds, minutes, hours, days_of_month, months, days_of_week = value + return { + CONF_SECONDS: validate_cron_seconds(seconds), + CONF_MINUTES: validate_cron_minutes(minutes), + CONF_HOURS: validate_cron_hours(hours), + CONF_DAYS_OF_MONTH: validate_cron_days_of_month(days_of_month), + CONF_MONTHS: validate_cron_months(months), + CONF_DAYS_OF_WEEK: validate_cron_days_of_week(days_of_week), + } + + +def validate_cron_keys(value): + if CONF_CRON in value: + for key in value.keys(): + if key in CRON_KEYS: + raise vol.Invalid("Cannot use option {} when cron: is specified.".format(key)) + cron_ = value[CONF_CRON] + value = {x: value[x] for x in value if x != CONF_CRON} + value.update(cron_) + return value + return cv.has_at_least_one_key(*CRON_KEYS)(value) + + TIME_PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_TIMEZONE, default=detect_tz): cv.string, + vol.Optional(CONF_ON_TIME): vol.All(cv.ensure_list, [vol.All(automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(CronTrigger), + vol.Optional(CONF_SECONDS): validate_cron_seconds, + vol.Optional(CONF_MINUTES): validate_cron_minutes, + vol.Optional(CONF_HOURS): validate_cron_hours, + vol.Optional(CONF_DAYS_OF_MONTH): validate_cron_days_of_month, + vol.Optional(CONF_MONTHS): validate_cron_months, + vol.Optional(CONF_DAYS_OF_WEEK): validate_cron_days_of_week, + vol.Optional(CONF_CRON): validate_cron_raw, + }), validate_cron_keys)]), }) def setup_time_core_(time_var, config): add(time_var.set_timezone(config[CONF_TIMEZONE])) + for conf in config.get(CONF_ON_TIME, []): + rhs = App.register_component(time_var.Pmake_cron_trigger()) + trigger = Pvariable(conf[CONF_TRIGGER_ID], rhs) + for second in conf.get(CONF_SECONDS, [x for x in range(0, 61)]): + add(trigger.add_second(second)) + for minute in conf.get(CONF_MINUTES, [x for x in range(0, 60)]): + add(trigger.add_minute(minute)) + for hour in conf.get(CONF_HOURS, [x for x in range(0, 24)]): + add(trigger.add_hour(hour)) + for day_of_month in conf.get(CONF_DAYS_OF_MONTH, [x for x in range(1, 32)]): + add(trigger.add_day_of_month(day_of_month)) + for month in conf.get(CONF_MONTHS, [x for x in range(1, 13)]): + add(trigger.add_month(month)) + for day_of_week in conf.get(CONF_DAYS_OF_WEEK, [x for x in range(1, 8)]): + add(trigger.add_day_of_week(day_of_week)) + automation.build_automation(trigger, NoArg, conf) + def setup_time(time_var, config): add_job(setup_time_core_, time_var, config) diff --git a/esphomeyaml/components/time/sntp.py b/esphomeyaml/components/time/sntp.py index 478610fdbd..19ba250b10 100644 --- a/esphomeyaml/components/time/sntp.py +++ b/esphomeyaml/components/time/sntp.py @@ -8,7 +8,7 @@ from esphomeyaml.helpers import App, Pvariable SNTPComponent = time_.time_ns.SNTPComponent PLATFORM_SCHEMA = time_.TIME_PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ID): cv.declare_variable_id(SNTPComponent), + cv.GenerateID(): cv.declare_variable_id(SNTPComponent), vol.Optional(CONF_SERVERS): vol.All(cv.ensure_list, [cv.string], vol.Length(max=3)), vol.Optional(CONF_LAMBDA): cv.lambda_, }) diff --git a/esphomeyaml/config.py b/esphomeyaml/config.py index 2b0e0f00a1..355a394ef4 100644 --- a/esphomeyaml/config.py +++ b/esphomeyaml/config.py @@ -7,62 +7,20 @@ from collections import OrderedDict import voluptuous as vol from voluptuous.humanize import humanize_error -import esphomeyaml.config_validation as cv -from esphomeyaml import core, yaml_util, automation -from esphomeyaml.const import CONF_BOARD, CONF_BOARD_FLASH_MODE, 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, CONF_ON_BOOT, CONF_TRIGGER_ID, \ - CONF_PRIORITY, CONF_ON_SHUTDOWN, CONF_BUILD_PATH +from esphomeyaml import core, yaml_util, core_config +from esphomeyaml.const import CONF_ESPHOMEYAML, CONF_PLATFORM, CONF_WIFI, ESP_PLATFORMS from esphomeyaml.core import ESPHomeYAMLError -from esphomeyaml.helpers import App, add, color, esphomelib_ns, Pvariable, NoArg, const_char_p +from esphomeyaml.helpers import color _LOGGER = logging.getLogger(__name__) -DEFAULT_LIBRARY_URI = u'https://github.com/OttoWinter/esphomelib.git#v1.7.0' - -BUILD_FLASH_MODES = ['qio', 'qout', 'dio', 'dout'] -StartupTrigger = esphomelib_ns.StartupTrigger -ShutdownTrigger = esphomelib_ns.ShutdownTrigger - -CORE_SCHEMA = vol.Schema({ - vol.Required(CONF_NAME): cv.valid_name, - 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, - vol.Optional(CONF_USE_BUILD_FLAGS, default=True): cv.boolean, - vol.Optional(CONF_BOARD_FLASH_MODE): vol.All(vol.Lower, cv.one_of(*BUILD_FLASH_MODES)), - vol.Optional(CONF_ON_BOOT): vol.All(cv.ensure_list, [automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(StartupTrigger), - vol.Optional(CONF_PRIORITY): vol.Coerce(float), - })]), - vol.Optional(CONF_ON_SHUTDOWN): vol.All(cv.ensure_list, [automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(ShutdownTrigger), - })]), - vol.Optional(CONF_BUILD_PATH): cv.string, -}) - REQUIRED_COMPONENTS = [ CONF_ESPHOMEYAML, CONF_WIFI ] - _COMPONENT_CACHE = {} _ALL_COMPONENTS = [] -def core_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_automation(trigger, NoArg, conf) - - for conf in config.get(CONF_ON_SHUTDOWN, []): - trigger = Pvariable(conf[CONF_TRIGGER_ID], ShutdownTrigger.new()) - automation.build_automation(trigger, const_char_p, conf) - - def get_component(domain): if domain in _COMPONENT_CACHE: return _COMPONENT_CACHE[domain] @@ -171,9 +129,9 @@ def validate_config(config): result.add_error(_format_config_error(ex, domain, config), domain, config) try: - result[CONF_ESPHOMEYAML] = CORE_SCHEMA(config[CONF_ESPHOMEYAML]) + result[CONF_ESPHOMEYAML] = core_config.CONFIG_SCHEMA(config[CONF_ESPHOMEYAML]) except vol.Invalid as ex: - _comp_error(ex, CONF_ESPHOMEYAML, config) + _comp_error(ex, CONF_ESPHOMEYAML, config[CONF_ESPHOMEYAML]) for domain, conf in config.iteritems(): domain = str(domain) @@ -288,23 +246,7 @@ def load_config(path): except OSError: raise ESPHomeYAMLError(u"Could not read configuration file at {}".format(path)) core.RAW_CONFIG = config - - if CONF_ESPHOMEYAML not in config: - raise ESPHomeYAMLError(u"No esphomeyaml section in config") - core_conf = config[CONF_ESPHOMEYAML] - if CONF_PLATFORM not in core_conf: - raise ESPHomeYAMLError("esphomeyaml.platform not specified.") - esp_platform = unicode(core_conf[CONF_PLATFORM]) - esp_platform = esp_platform.upper() - 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 - if CONF_BOARD not in core_conf: - raise ESPHomeYAMLError("esphomeyaml.board not specified.") - core.BOARD = unicode(core_conf[CONF_BOARD]) - core.SIMPLIFY = cv.boolean(core_conf.get(CONF_SIMPLIFY, True)) + core_config.preload_core_config(config) try: result = validate_config(config) diff --git a/esphomeyaml/config_validation.py b/esphomeyaml/config_validation.py index 33befdc342..980f8c125b 100644 --- a/esphomeyaml/config_validation.py +++ b/esphomeyaml/config_validation.py @@ -3,12 +3,13 @@ from __future__ import print_function import logging +import os import re import uuid as uuid_ import voluptuous as vol -from esphomeyaml import core +from esphomeyaml import core, helpers from esphomeyaml.const import CONF_AVAILABILITY, CONF_COMMAND_TOPIC, CONF_DISCOVERY, CONF_ID, \ CONF_NAME, CONF_PAYLOAD_AVAILABLE, \ CONF_PAYLOAD_NOT_AVAILABLE, CONF_PLATFORM, CONF_RETAIN, CONF_STATE_TOPIC, CONF_TOPIC, \ @@ -240,6 +241,19 @@ def has_exactly_one_key(*keys): return validate +def has_at_most_one_key(*keys): + def validate(obj): + if not isinstance(obj, dict): + raise vol.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))) + return obj + + return validate + + TIME_PERIOD_ERROR = "Time period {} should be format number + unit, for example 5ms, 5s, 5min, 5h" time_period_dict = vol.All( @@ -598,6 +612,28 @@ def dimensions(value): return dimensions([match.group(1), match.group(2)]) +def directory(value): + value = string(value) + path = helpers.relative_path(value) + if not os.path.exists(path): + raise vol.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)) + return value + + +def file_(value): + value = string(value) + path = helpers.relative_path(value) + if not os.path.exists(path): + raise vol.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)) + return value + + REGISTERED_IDS = set() diff --git a/esphomeyaml/const.py b/esphomeyaml/const.py index 16505c4efc..ac643a71d9 100644 --- a/esphomeyaml/const.py +++ b/esphomeyaml/const.py @@ -5,6 +5,7 @@ MINOR_VERSION = 7 PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) +ESPHOMELIB_VERSION = '1.7.0' ESP_PLATFORM_ESP32 = 'ESP32' ESP_PLATFORM_ESP8266 = 'ESP8266' @@ -16,9 +17,14 @@ CONF_ESPHOMEYAML = 'esphomeyaml' CONF_NAME = 'name' CONF_PLATFORM = 'platform' CONF_BOARD = 'board' -CONF_SIMPLIFY = 'simplify' -CONF_USE_BUILD_FLAGS = 'use_build_flags' -CONF_LIBRARY_URI = 'library_uri' +CONF_ESPHOMELIB_VERSION = 'esphomelib_version' +CONF_USE_CUSTOM_CODE = 'use_custom_code' +CONF_ARDUINO_VERSION = 'arduino_version' +CONF_LOCAL = 'local' +CONF_REPOSITORY = 'repository' +CONF_COMMIT = 'commit' +CONF_TAG = 'tag' +CONF_BRANCH = 'branch' CONF_LOGGER = 'logger' CONF_WIFI = 'wifi' CONF_SSID = 'ssid' @@ -323,27 +329,17 @@ CONF_COLD_WHITE = 'cold_white' CONF_WARM_WHITE = 'warm_white' CONF_COLD_WHITE_COLOR_TEMPERATURE = 'cold_white_color_temperature' CONF_WARM_WHITE_COLOR_TEMPERATURE = 'warm_white_color_temperature' - -ESP32_BOARDS = [ - 'featheresp32', 'node32s', 'espea32', 'firebeetle32', 'esp32doit-devkit-v1', - 'pocket_32', 'espectro32', 'esp32vn-iot-uno', 'esp320', 'esp-wrover-kit', - 'esp32dev', 'heltec_wifi_kit32', 'heltec_wifi_lora_32', 'hornbill32dev', - 'hornbill32minima', 'intorobot', 'm5stack-core-esp32', 'mhetesp32devkit', - 'mhetesp32minikit', 'nano32', 'microduino-core-esp32', 'nodemcu-32s', - 'quantum', 'esp32-evb', 'esp32-gateway', 'onehorse32dev', 'esp32thing', - 'espino32', 'lolin32', 'wemosbat', 'widora-air', 'nina_w10', -] - -ESP8266_BOARDS = [ - 'gen4iod', 'huzzah', 'oak', 'espduino', 'espectro', 'espresso_lite_v1', - 'espresso_lite_v2', 'espino', 'esp01', 'esp01_1m', 'esp07', 'esp12e', 'esp8285', - 'esp_wroom_02', 'phoenix_v1', 'phoenix_v2', 'wifinfo', 'heltex_wifi_kit_8', - 'nodemcu', 'nodemcuv2', 'modwifi', 'wio_node', 'sparkfunBlynk', 'thing', - 'thingdev', 'esp210', 'espinotee', 'd1', 'd1_mini', 'd1_mini_lite', 'd1_mini_pro', -] -ESP_BOARDS_FOR_PLATFORM = { - ESP_PLATFORM_ESP32: ESP32_BOARDS, - ESP_PLATFORM_ESP8266: ESP8266_BOARDS -} +CONF_ON_LOOP = 'on_loop' +CONF_ON_TIME = 'on_time' +CONF_SECONDS = 'seconds' +CONF_MINUTES = 'minutes' +CONF_HOURS = 'hours' +CONF_DAYS_OF_MONTH = 'days_of_month' +CONF_MONTHS = 'months' +CONF_DAYS_OF_WEEK = 'days_of_week' +CONF_CRON = 'cron' ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_' +ARDUINO_VERSION_ESP32_DEV = 'https://github.com/platformio/platform-espressif32.git#feature/stage' +ARDUINO_VERSION_ESP8266_DEV = 'https://github.com/platformio/platform-espressif8266.git#feature' \ + '/stage' diff --git a/esphomeyaml/core.py b/esphomeyaml/core.py index edb96b2836..60589d6e24 100644 --- a/esphomeyaml/core.py +++ b/esphomeyaml/core.py @@ -239,7 +239,6 @@ class ID(object): CONFIG_PATH = None -SIMPLIFY = True ESP_PLATFORM = '' BOARD = '' RAW_CONFIG = None diff --git a/esphomeyaml/core_config.py b/esphomeyaml/core_config.py new file mode 100644 index 0000000000..75e0c0c01d --- /dev/null +++ b/esphomeyaml/core_config.py @@ -0,0 +1,199 @@ +import os +import re + +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml import automation, core, pins +from esphomeyaml.const import CONF_ARDUINO_VERSION, CONF_BOARD, CONF_BOARD_FLASH_MODE, \ + CONF_BRANCH, CONF_BUILD_PATH, CONF_COMMIT, CONF_ESPHOMELIB_VERSION, CONF_ESPHOMEYAML, \ + CONF_LOCAL, CONF_NAME, CONF_ON_BOOT, CONF_ON_LOOP, CONF_ON_SHUTDOWN, CONF_PLATFORM, \ + CONF_PRIORITY, CONF_REPOSITORY, CONF_TAG, CONF_TRIGGER_ID, CONF_USE_CUSTOM_CODE, \ + ESPHOMELIB_VERSION, ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266, ARDUINO_VERSION_ESP8266_DEV, \ + ARDUINO_VERSION_ESP32_DEV +from esphomeyaml.core import ESPHomeYAMLError +from esphomeyaml.helpers import App, NoArg, Pvariable, add, const_char_p, esphomelib_ns, \ + relative_path + +LIBRARY_URI_REPO = u'https://github.com/OttoWinter/esphomelib.git' + +BUILD_FLASH_MODES = ['qio', 'qout', 'dio', 'dout'] +StartupTrigger = esphomelib_ns.StartupTrigger +ShutdownTrigger = esphomelib_ns.ShutdownTrigger +LoopTrigger = esphomelib_ns.LoopTrigger + +VERSION_REGEX = re.compile(r'^[0-9]+\.[0-9]+\.[0-9]+(?:-beta)?(?:-alpha)?$') + + +def validate_board(value): + if core.ESP_PLATFORM == ESP_PLATFORM_ESP8266: + board_pins = pins.ESP8266_BOARD_PINS + elif core.ESP_PLATFORM == ESP_PLATFORM_ESP32: + board_pins = pins.ESP32_BOARD_PINS + else: + raise NotImplementedError + + if value not in board_pins: + raise vol.Invalid(u"Could not find board '{}'. Valid boards are {}".format( + value, u', '.join(pins.ESP8266_BOARD_PINS.keys()))) + return value + + +def validate_simple_esphomelib_version(value): + value = cv.string_strict(value) + if value.upper() == 'LATEST': + return LIBRARY_URI_REPO + '#v{}'.format(ESPHOMELIB_VERSION) + elif value.upper() == 'DEV': + return LIBRARY_URI_REPO + elif VERSION_REGEX.match(value) is not None: + return LIBRARY_URI_REPO + '#v{}'.format(value) + return value + + +def validate_local_esphomelib_version(value): + value = cv.directory(value) + path = 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"esphomelib copy.".format(library_json, value)) + return value + + +def convert_esphomelib_version_schema(value): + if CONF_COMMIT in value: + return value[CONF_REPOSITORY] + '#' + value[CONF_COMMIT] + if CONF_BRANCH in value: + return value[CONF_REPOSITORY] + '#' + value[CONF_BRANCH] + return value[CONF_REPOSITORY] + '#' + value[CONF_TAG] + + +ESPHOMELIB_VERSION_SCHEMA = vol.Any( + validate_simple_esphomelib_version, + vol.Schema({ + vol.Required(CONF_LOCAL): validate_local_esphomelib_version, + }), + vol.All( + vol.Schema({ + vol.Optional(CONF_REPOSITORY, default=LIBRARY_URI_REPO): cv.string, + vol.Optional(CONF_COMMIT, 'tag'): cv.string, + vol.Optional(CONF_BRANCH, 'tag'): cv.string, + vol.Optional(CONF_TAG, 'tag'): cv.string, + }), + cv.has_at_most_one_key(CONF_COMMIT, CONF_BRANCH, CONF_TAG), + convert_esphomelib_version_schema + ), +) + + +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)) + + +PLATFORMIO_ESP8266_LUT = { + '2.4.2': 'espressif8266@1.8.0', + '2.4.1': 'espressif8266@1.7.3', + '2.4.0': 'espressif8266@1.6.0', + '2.3.0': 'espressif8266@1.5.0', + 'RECOMMENDED': 'espressif8266@>=1.8.0', + 'LATEST': 'espressif8266', + 'DEV': ARDUINO_VERSION_ESP8266_DEV, +} + +PLATFORMIO_ESP32_LUT = { + '1.0.0': 'espressif32@1.3.0', + 'RECOMMENDED': 'espressif32@>=1.3.0', + 'LATEST': 'espressif32', + 'DEV': ARDUINO_VERSION_ESP32_DEV, +} + + +def validate_arduino_version(value): + value = cv.string_strict(value) + value_ = value.upper() + if core.ESP_PLATFORM == ESP_PLATFORM_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@") + if value_ in PLATFORMIO_ESP8266_LUT: + return PLATFORMIO_ESP8266_LUT[value_] + return value + elif core.ESP_PLATFORM == ESP_PLATFORM_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@") + if value_ in PLATFORMIO_ESP32_LUT: + return PLATFORMIO_ESP32_LUT[value_] + return value + else: + raise NotImplementedError + + +CONFIG_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.valid_name, + vol.Required(CONF_PLATFORM): vol.All(vol.Upper, cv.one_of('ESP8266', 'ESPRESSIF8266', + 'ESP32', 'ESPRESSIF32')), + vol.Required(CONF_BOARD): validate_board, + vol.Optional(CONF_ESPHOMELIB_VERSION, default='latest'): ESPHOMELIB_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): cv.string, + + vol.Optional(CONF_BOARD_FLASH_MODE): vol.All(vol.Lower, cv.one_of(*BUILD_FLASH_MODES)), + vol.Optional(CONF_ON_BOOT): vol.All(cv.ensure_list, [automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(StartupTrigger), + vol.Optional(CONF_PRIORITY): vol.Coerce(float), + })]), + vol.Optional(CONF_ON_SHUTDOWN): vol.All(cv.ensure_list, [automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(ShutdownTrigger), + })]), + vol.Optional(CONF_ON_LOOP): vol.All(cv.ensure_list, [automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(LoopTrigger), + })]), + + vol.Optional('library_uri'): cv.invalid("The library_uri option has been removed in 1.8.0 and " + "was moved into the esphomelib_version option.") +}) + + +def preload_core_config(config): + if CONF_ESPHOMEYAML not in config: + raise ESPHomeYAMLError(u"No esphomeyaml section in config") + core_conf = config[CONF_ESPHOMEYAML] + if CONF_PLATFORM not in core_conf: + raise ESPHomeYAMLError("esphomeyaml.platform not specified.") + if CONF_BOARD not in core_conf: + raise ESPHomeYAMLError("esphomeyaml.board not specified.") + + try: + core.ESP_PLATFORM = validate_platform(core_conf[CONF_PLATFORM]) + core.BOARD = validate_board(core_conf[CONF_BOARD]) + except vol.Invalid as e: + raise ESPHomeYAMLError(unicode(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_automation(trigger, NoArg, conf) + + for conf in config.get(CONF_ON_SHUTDOWN, []): + trigger = Pvariable(conf[CONF_TRIGGER_ID], ShutdownTrigger.new()) + automation.build_automation(trigger, const_char_p, conf) + + for conf in config.get(CONF_ON_LOOP, []): + rhs = App.register_component(LoopTrigger.new()) + trigger = Pvariable(conf[CONF_TRIGGER_ID], rhs) + automation.build_automation(trigger, NoArg, conf) diff --git a/esphomeyaml/helpers.py b/esphomeyaml/helpers.py index 21e7ea0ce7..fd1ba8b604 100644 --- a/esphomeyaml/helpers.py +++ b/esphomeyaml/helpers.py @@ -2,6 +2,7 @@ from __future__ import print_function import inspect import logging +import os import re from collections import OrderedDict, deque @@ -85,8 +86,6 @@ class AssignmentExpression(Expression): def __str__(self): type_ = self.type - if core.SIMPLIFY: - type_ = u'auto' return u"{} {}{} = {}".format(type_, self.modifier, self.name, self.rhs) def has_side_effects(self): @@ -662,3 +661,7 @@ def color(the_color, message='', reset=None): if not message: return parse_colors(the_color) return parse_colors(the_color) + message + escape_codes[reset or 'reset'] + + +def relative_path(path): + return os.path.join(os.path.dirname(core.CONFIG_PATH), os.path.expanduser(path)) diff --git a/esphomeyaml/pins.py b/esphomeyaml/pins.py index 0521de44c7..1b67860ed4 100644 --- a/esphomeyaml/pins.py +++ b/esphomeyaml/pins.py @@ -10,59 +10,162 @@ from esphomeyaml.const import CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_PCF857 _LOGGER = logging.getLogger(__name__) -ESP8266_PINS = { - 'A0': 17, 'SS': 15, 'MOSI': 13, 'MISO': 12, 'SCK': 14, -} -ESP8266_NODEMCU_PINS = dict(ESP8266_PINS, **{ - 'D0': 16, 'D1': 5, 'D2': 4, 'D3': 0, 'D4': 2, 'D5': 14, 'D6': 12, 'D7': 13, 'D8': 15, 'D9': 3, - 'D10': 1, 'LED': 16, 'SDA': 4, 'SCL': 5, -}) -ESP8266_D1_PINS = dict(ESP8266_PINS, **{ - 'D0': 3, 'D1': 1, 'D2': 16, 'D3': 5, 'D4': 4, 'D5': 14, 'D6': 12, 'D7': 13, 'D8': 0, 'D9': 2, - 'D10': 15, 'D11': 13, 'D12': 14, 'D13': 14, 'D14': 4, 'D15': 5, 'LED': 2, 'SDA': 4, 'SCL': 5, -}) -ESP8266_D1_MINI_PINS = dict(ESP8266_PINS, **{ - 'D0': 16, 'D1': 5, 'D2': 4, 'D3': 0, 'D4': 2, 'D5': 14, 'D6': 12, 'D7': 13, 'D8': 15, 'RX': 3, - 'TX': 1, 'LED': 2, 'SDA': 4, 'SCL': 5, -}) -ESP8266_THING_PINS = dict(ESP8266_PINS, **{ - 'LED': 5, 'SDA': 2, 'SCL': 14, -}) -ESP8266_ADAFRUIT_PINS = dict(ESP8266_PINS, **{ - 'LED': 0, 'SDA': 4, 'SCL': 5, -}) -ESP8266_ESPDUINO_PINS = dict(ESP8266_PINS, **{ - 'LED': 16, 'SDA': 4, 'SCL': 5, -}) -ESP8266_BOARD_TO_PINS = { - 'huzzah': ESP8266_ADAFRUIT_PINS, - 'espduino': ESP8266_ESPDUINO_PINS, - 'nodemcu': ESP8266_NODEMCU_PINS, 'nodemcuv2': ESP8266_NODEMCU_PINS, - 'thing': ESP8266_THING_PINS, 'thingdev': ESP8266_THING_PINS, - 'd1': ESP8266_D1_PINS, - 'd1_mini': ESP8266_D1_MINI_PINS, 'd1_mini_lite': ESP8266_D1_MINI_PINS, - 'd1_mini_pro': ESP8266_D1_MINI_PINS +ESP8266_BASE_PINS = { + 'A0': 17, 'SS': 15, 'MOSI': 13, 'MISO': 12, 'SCK': 14, 'SDA': 4, 'SCL': 5, 'RX': 3, 'TX': 1 } -ESP32_PINS = { +ESP8266_BOARD_PINS = { + 'd1': {'D0': 3, 'D1': 1, 'D2': 16, 'D3': 5, 'D4': 4, 'D5': 14, 'D6': 12, 'D7': 13, 'D8': 0, + 'D9': 2, 'D10': 15, 'D11': 13, 'D12': 14, 'D13': 14, 'D14': 4, 'D15': 5, 'LED': 2}, + 'd1_mini': {'D0': 16, 'D1': 5, 'D2': 4, 'D3': 0, 'D4': 2, 'D5': 14, 'D6': 12, 'D7': 13, + 'D8': 15, 'LED': 2}, + 'd1_mini_lite': 'd1_mini', + 'd1_mini_pro': 'd1_mini', + 'esp01': {}, + 'esp01_1m': {}, + 'esp07': {}, + 'esp12e': {}, + 'esp210': {}, + 'esp8285': {}, + 'esp_wroom_02': {}, + 'espduino': {'LED': 16}, + 'espectro': {'LED': 15, 'BUTTON': 2}, + 'espino': {'LED': 2, 'LED_RED': 2, 'LED_GREEN': 4, 'LED_BLUE': 5, 'BUTTON': 0}, + 'espinotee': {'LED': 16}, + 'espresso_lite_v1': {'LED': 16}, + 'espresso_lite_v2': {'LED': 2}, + 'gen4iod': {}, + 'heltec_wifi_kit_8': 'd1_mini', + 'huzzah': {'LED': 0}, + 'modwifi': {}, + 'nodemcu': {'D0': 16, 'D1': 5, 'D2': 4, 'D3': 0, 'D4': 2, 'D5': 14, 'D6': 12, 'D7': 13, + 'D8': 15, 'D9': 3, 'D10': 1, 'LED': 16}, + 'nodemcuv2': 'nodemcu', + 'oak': {'P0': 2, 'P1': 5, 'P2': 0, 'P3': 3, 'P4': 1, 'P5': 4, 'P6': 15, 'P7': 13, 'P8': 12, + 'P9': 14, 'P10': 16, 'P11': 17, 'LED': 5}, + 'phoenix_v1': {'LED': 16}, + 'phoenix_v2': {'LED': 2}, + 'sparkfunBlynk': 'thing', + 'thing': {'LED': 5, 'SDA': 2, 'SCL': 14}, + 'thingdev': 'thing', + 'wifi_slot': {'LED': 2}, + 'wifiduino': {'D0': 3, 'D1': 1, 'D2': 2, 'D3': 0, 'D4': 4, 'D5': 5, 'D6': 16, 'D7': 14, + 'D8': 12, 'D9': 13, 'D10': 15, 'D11': 13, 'D12': 12, 'D13': 14}, + 'wifinfo': {'LED': 12, 'D0': 16, 'D1': 5, 'D2': 4, 'D3': 0, 'D4': 2, 'D5': 14, 'D6': 12, + 'D7': 13, 'D8': 15, 'D9': 3, 'D10': 1}, + 'wio_link': {'LED': 2, 'GROVE': 15}, + 'wio_node': 'nodemcu', + 'xinabox_cw01': {'SDA': 2, 'SCL': 14, 'LED': 5, 'LED_RED': 12, 'LED_GREEN': 13} +} + +ESP32_BASE_PINS = { 'TX': 1, 'RX': 3, 'SDA': 21, 'SCL': 22, 'SS': 5, 'MOSI': 23, 'MISO': 19, 'SCK': 18, 'A0': 36, 'A3': 39, 'A4': 32, 'A5': 33, 'A6': 34, 'A7': 35, 'A10': 4, 'A11': 0, 'A12': 2, 'A13': 15, 'A14': 13, 'A15': 12, 'A16': 14, 'A17': 27, 'A18': 25, 'A19': 26, 'T0': 4, 'T1': 0, 'T2': 2, - 'T3': 15, 'T4': 12, 'T5': 12, 'T6': 14, 'T7': 27, 'T8': 33, 'T9': 32, 'DAC1': 25, 'DAC2': 26, + 'T3': 15, 'T4': 13, 'T5': 12, 'T6': 14, 'T7': 27, 'T8': 33, 'T9': 32, 'DAC1': 25, 'DAC2': 26, 'SVP': 36, 'SVN': 39, } -ESP32_NODEMCU_32S_PINS = dict(ESP32_PINS, **{ - 'LED': 2, -}) -ESP32_LOLIN32_PINS = dict(ESP32_PINS, **{ - 'LED': 5 -}) -ESP32_BOARD_TO_PINS = { - 'nodemcu-32s': ESP32_NODEMCU_32S_PINS, - 'lolin32': ESP32_LOLIN32_PINS, + +ESP32_BOARD_PINS = { + 'alksesp32': {'D0': 40, 'D1': 41, 'D2': 15, 'D3': 2, 'D4': 0, 'D5': 4, 'D6': 16, 'D7': 17, + 'D8': 5, 'D9': 18, 'D10': 19, 'D11': 21, 'D12': 22, 'D13': 23, 'A0': 32, 'A1': 33, + 'A2': 25, 'A3': 26, 'A4': 27, 'A5': 14, 'A6': 12, 'A7': 15, 'L_R': 22, 'L_G': 17, + 'L_Y': 23, 'L_B': 5, 'L_RGB_R': 4, 'L_RGB_G': 21, 'L_RGB_B': 16, 'SW1': 15, + 'SW2': 2, 'SW3': 0, 'POT1': 32, 'POT2': 33, 'PIEZO1': 19, 'PIEZO2': 18, + 'PHOTO': 25, 'DHT_PIN': 26, 'S1': 4, 'S2': 16, 'S3': 18, 'S4': 19, 'S5': 21, + 'SDA': 27, 'SCL': 14, 'SS': 19, 'MOSI': 21, 'MISO': 22, 'SCK': 23}, + 'esp-wrover-kit': {}, + 'esp32-evb': {'BUTTON': 34, 'SDA': 13, 'SCL': 16, 'SS': 17, 'MOSI': 2, 'MISO': 15, 'SCK': 14}, + 'esp32-gateway': {'LED': 33, 'BUTTON': 34, 'SCL': 16, 'SDA': 17}, + 'esp320': {'LED': 5, 'SDA': 2, 'SCL': 14, 'SS': 15, 'MOSI': 13, 'MISO': 12, 'SCK': 14}, + 'esp32dev': {}, + 'esp32doit-devkit-v1': {'LED': 2}, + 'esp32thing': {'LED': 5, 'BUTTON': 0, 'SS': 2}, + 'esp32vn-iot-uno': {}, + 'espea32': {'LED': 5, 'BUTTON': 0}, + 'espectro32': {'LED': 15, 'SD_SS': 33}, + 'espino32': {'LED': 16, 'BUTTON': 0}, + 'featheresp32': {'LED': 13, 'TX': 17, 'RX': 16, 'SDA': 23, 'SS': 2, 'MOSI': 18, 'SCK': 5, + 'A0': 26, 'A1': 25, 'A2': 34, 'A4': 36, 'A5': 4, 'A6': 14, 'A7': 32, 'A8': 15, + 'A9': 33, 'A10': 27, 'A11': 12, 'A12': 13, 'A13': 35}, + 'firebeetle32': {'LED': 2}, + 'heltec_wifi_kit_32': {'LED': 25, 'BUTTON': 0, 'A1': 37, 'A2': 38}, + 'heltec_wifi_lora_32': {'LED': 25, 'BUTTON': 0, 'SDA': 4, 'SCL': 15, 'SS': 18, 'MOSI': 27, + 'SCK': 5, 'A1': 37, 'A2': 38, 'T8': 32, 'T9': 33, 'DAC1': 26, + 'DAC2': 25, 'OLED_SCL': 15, 'OLED_SDA': 4, 'OLED_RST': 16, + 'LORA_SCK': 5, 'LORA_MOSI': 27, 'LORA_MISO': 19, 'LORA_CS': 18, + 'LORA_RST': 14, 'LORA_IRQ': 26}, + 'hornbill32dev': {'LED': 13, 'BUTTON': 0}, + 'hornbill32minima': {'SS': 2}, + 'intorobot': {'LED': 4, 'LED_RED': 27, 'LED_GREEN': 21, 'LED_BLUE': 22, + 'BUTTON': 0, 'SDA': 23, 'SCL': 19, 'MOSI': 16, 'MISO': 17, 'A1': 39, 'A2': 35, + 'A3': 25, 'A4': 26, 'A5': 14, 'A6': 12, 'A7': 15, 'A8': 13, 'A9': 2, 'D0': 19, + 'D1': 23, 'D2': 18, 'D3': 17, 'D4': 16, 'D5': 5, 'D6': 4, 'T0': 19, 'T1': 23, + 'T2': 18, 'T3': 17, 'T4': 16, 'T5': 5, 'T6': 4}, + 'lolin32': {'LED': 5}, + 'lolin_d32': {'LED': 5, 'VBAT': 35}, + 'lolin_d32_pro': {'LED': 5, 'VBAT': 35, 'TF_CS': 4, 'TS_CS': 12, 'TFT_CS': 14, 'TFT_LED': 32, + 'TFT_RST': 33, 'TFT_DC': 27}, + 'm5stack-core-esp32': {'TXD2': 17, 'RXD2': 16, 'G23': 23, 'G19': 19, 'G18': 18, 'G3': 3, + 'G16': 16, 'G21': 21, 'G2': 2, 'G12': 12, 'G15': 15, 'G35': 35, + 'G36': 36, 'G25': 25, 'G26': 26, 'G1': 1, 'G17': 17, 'G22': 22, 'G5': 5, + 'G13': 13, 'G0': 0, 'G34': 34, 'ADC1': 35, 'ADC2': 36}, + 'm5stack-fire': {'G23': 23, 'G19': 19, 'G18': 18, 'G3': 3, 'G16': 16, 'G21': 21, 'G2': 2, + 'G12': 12, 'G15': 15, 'G35': 35, 'G36': 36, 'G25': 25, 'G26': 26, 'G1': 1, + 'G17': 17, 'G22': 22, 'G5': 5, 'G13': 13, 'G0': 0, 'G34': 34, 'ADC1': 35, + 'ADC2': 36}, + 'mhetesp32devkit': {'LED': 2}, + 'mhetesp32minikit': {'LED': 2}, + 'microduino-core-esp32': {'SDA': 22, 'SCL': 21, 'SDA1': 12, 'SCL1': 13, 'A0': 12, 'A1': 13, + 'A2': 15, 'A3': 4, 'A6': 38, 'A7': 37, 'A8': 32, 'A9': 33, 'A10': 25, + 'A11': 26, 'A12': 27, 'A13': 14, 'D0': 3, 'D1': 1, 'D2': 16, 'D3': 17, + 'D4': 32, 'D5': 33, 'D6': 25, 'D7': 26, 'D8': 27, 'D9': 14, 'D10': 5, + 'D11': 23, 'D12': 19, 'D13': 18, 'D14': 12, 'D15': 13, 'D16': 15, + 'D17': 4, 'D18': 22, 'D19': 21, 'D20': 38, 'D21': 37}, + 'nano32': {'LED': 16, 'BUTTON': 0}, + 'nina_w10': {'LED_GREEN': 33, 'LED_RED': 23, 'LED_BLUE': 21, 'SW1': 33, 'SW2': 27, 'SDA': 12, + 'SCL': 13, 'D0': 3, 'D1': 1, 'D2': 26, 'D3': 25, 'D4': 35, 'D5': 27, 'D6': 22, + 'D7': 0, 'D8': 15, 'D9': 14, 'D10': 5, 'D11': 19, 'D12': 23, 'D13': 18, 'D14': 13, + 'D15': 12, 'D16': 32, 'D17': 33, 'D18': 21, 'D19': 34, 'D20': 36, 'D21': 39}, + 'node32s': {}, + 'nodemcu-32s': {'LED': 2, 'BUTTON': 0}, + 'odroid_esp32': {'LED': 2, 'SDA': 15, 'SCL': 4, 'SS': 22, 'ADC1': 35, 'ADC2': 36}, + 'onehorse32dev': {'LED': 5, 'BUTTON': 0, 'A1': 37, 'A2': 38}, + 'pico32': {}, + 'pocket_32': {'LED': 16}, + 'quantum': {}, + 'ttgo-lora32-v1': {'LED': 2, 'BUTTON': 0, 'SS': 18, 'MOSI': 27, 'SCK': 5, 'A1': 37, 'A2': 38, + 'T8': 32, 'T9': 33, 'DAC1': 26, 'DAC2': 25, 'OLED_SDA': 4, 'OLED_SCL': 15, + 'OLED_RST': 16, 'LORA_SCK': 5, 'LORA_MISO': 19, 'LORA_MOSI': 27, + 'LORA_CS': 18, 'LORA_RST': 14, 'LORA_IRQ': 26}, + 'wemosbat': 'pocket_32', + 'widora-air': {'LED': 25, 'BUTTON': 0, 'SDA': 23, 'SCL': 19, 'MOSI': 16, 'MISO': 17, 'A1': 39, + 'A2': 35, 'A3': 25, 'A4': 26, 'A5': 14, 'A6': 12, 'A7': 15, 'A8': 13, 'A9': 2, + 'D0': 19, 'D1': 23, 'D2': 18, 'D3': 17, 'D4': 16, 'D5': 5, 'D6': 4, 'T0': 19, + 'T1': 23, 'T2': 18, 'T3': 17, 'T4': 16, 'T5': 5, 'T6': 4}, + 'xinabox_cw02': {'LED': 27}, } +def _lookup_pin(platform, board, value): + if platform == ESP_PLATFORM_ESP8266: + board_pins = ESP8266_BOARD_PINS.get(board, {}) + base_pins = ESP8266_BASE_PINS + elif platform == ESP_PLATFORM_ESP32: + board_pins = ESP32_BOARD_PINS.get(board, {}) + base_pins = ESP32_BASE_PINS + else: + raise NotImplementedError + + if isinstance(board_pins, str): + return _lookup_pin(platform, board_pins, value) + if value in board_pins: + return board_pins[value] + if value in base_pins: + return base_pins[value] + raise vol.Invalid(u"Can't find internal pin number for {}.".format(value)) + + 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 " @@ -75,27 +178,7 @@ def _translate_pin(value): pass if value.startswith('GPIO'): return vol.Coerce(int)(value[len('GPIO'):].strip()) - if core.ESP_PLATFORM == ESP_PLATFORM_ESP32: - if value in ESP32_PINS: - return ESP32_PINS[value] - if core.BOARD not in ESP32_BOARD_TO_PINS: - raise vol.Invalid(u"ESP32: Unknown board {} with unknown " - u"pin {}.".format(core.BOARD, value)) - if value not in ESP32_BOARD_TO_PINS[core.BOARD]: - raise vol.Invalid(u"ESP32: Board {} doesn't have " - u"pin {}".format(core.BOARD, value)) - return ESP32_BOARD_TO_PINS[core.BOARD][value] - elif core.ESP_PLATFORM == ESP_PLATFORM_ESP8266: - if value in ESP8266_PINS: - return ESP8266_PINS[value] - if core.BOARD not in ESP8266_BOARD_TO_PINS: - raise vol.Invalid(u"ESP8266: Unknown board {} with unknown " - u"pin {}.".format(core.BOARD, value)) - if value not in ESP8266_BOARD_TO_PINS[core.BOARD]: - raise vol.Invalid(u"ESP8266: Board {} doesn't have " - u"pin {}".format(core.BOARD, value)) - return ESP8266_BOARD_TO_PINS[core.BOARD][value] - raise vol.Invalid(u"Invalid ESP platform.") + return _lookup_pin(core.ESP_PLATFORM, core.BOARD, value) def validate_gpio_pin(value): diff --git a/esphomeyaml/wizard.py b/esphomeyaml/wizard.py index 23de624bf3..6b5c43471c 100644 --- a/esphomeyaml/wizard.py +++ b/esphomeyaml/wizard.py @@ -8,12 +8,13 @@ import voluptuous as vol import esphomeyaml.config_validation as cv from esphomeyaml.components import mqtt -from esphomeyaml.const import ESP_BOARDS_FOR_PLATFORM, ESP_PLATFORMS, ESP_PLATFORM_ESP32, \ - ESP_PLATFORM_ESP8266 +from esphomeyaml.const import ESP_PLATFORMS, ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266 from esphomeyaml.helpers import color # pylint: disable=anomalous-backslash-in-string +from esphomeyaml.pins import ESP32_BOARD_PINS, ESP8266_BOARD_PINS + CORE_BIG = """ _____ ____ _____ ______ / ____/ __ \| __ \| ____| | | | | | | |__) | |__ @@ -187,11 +188,12 @@ def wizard(path): # Don't sleep because user needs to copy link if platform == ESP_PLATFORM_ESP32: print("For example \"{}\".".format(color("bold_white", 'nodemcu-32s'))) + boards = list(ESP32_BOARD_PINS.keys()) else: print("For example \"{}\".".format(color("bold_white", 'nodemcuv2'))) + boards = list(ESP8266_BOARD_PINS.keys()) while True: board = raw_input(color("bold_white", "(board): ")) - boards = ESP_BOARDS_FOR_PLATFORM[platform] try: board = vol.All(vol.Lower, vol.Any(*boards))(board) break diff --git a/esphomeyaml/writer.py b/esphomeyaml/writer.py index 69bc4e4bba..8bd463a6b4 100644 --- a/esphomeyaml/writer.py +++ b/esphomeyaml/writer.py @@ -2,14 +2,17 @@ from __future__ import print_function import codecs import errno +import json import os from esphomeyaml import core from esphomeyaml.config import iter_components -from esphomeyaml.const import CONF_BOARD, CONF_BOARD_FLASH_MODE, CONF_ESPHOMEYAML, \ - CONF_LIBRARY_URI, \ - CONF_NAME, CONF_PLATFORM, CONF_USE_BUILD_FLAGS, ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266 +from esphomeyaml.const import CONF_ARDUINO_VERSION, CONF_BOARD, CONF_BOARD_FLASH_MODE, \ + CONF_ESPHOMELIB_VERSION, CONF_ESPHOMEYAML, CONF_LOCAL, CONF_NAME, CONF_USE_CUSTOM_CODE, \ + ESP_PLATFORM_ESP32, ARDUINO_VERSION_ESP32_DEV from esphomeyaml.core import ESPHomeYAMLError +from esphomeyaml.core_config import VERSION_REGEX +from esphomeyaml.helpers import relative_path CPP_AUTO_GENERATE_BEGIN = u'// ========== AUTO GENERATED CODE BEGIN ===========' CPP_AUTO_GENERATE_END = u'// =========== AUTO GENERATED CODE END ============' @@ -59,11 +62,6 @@ build_flags = ${{common.build_flags}} """ -PLATFORM_TO_PLATFORMIO = { - ESP_PLATFORM_ESP32: 'espressif32', - ESP_PLATFORM_ESP8266: 'espressif8266' -} - def get_build_flags(config, key): build_flags = set() @@ -81,19 +79,16 @@ def get_build_flags(config, key): return build_flags -def get_ini_content(config): +def get_ini_content(config, path): version_specific_settings = determine_platformio_version_settings() - 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, + u'platform': config[CONF_ESPHOMEYAML][CONF_ARDUINO_VERSION], u'board': config[CONF_ESPHOMEYAML][CONF_BOARD], u'build_flags': u'', } build_flags = set() - if config[CONF_ESPHOMEYAML][CONF_USE_BUILD_FLAGS]: + if not config[CONF_ESPHOMEYAML][CONF_USE_CUSTOM_CODE]: build_flags |= get_build_flags(config, 'build_flags') build_flags |= get_build_flags(config, 'BUILD_FLAGS') build_flags.add(u"-DESPHOMEYAML_USE") @@ -106,13 +101,48 @@ def get_ini_content(config): options[u'build_flags'] = u'\n '.join(build_flags) lib_deps = set() - lib_deps.add(config[CONF_ESPHOMEYAML][CONF_LIBRARY_URI]) + + lib_version = config[CONF_ESPHOMEYAML][CONF_ESPHOMELIB_VERSION] + lib_path = os.path.join(path, 'lib') + dst_path = os.path.join(lib_path, 'esphomelib') + if isinstance(lib_version, (str, unicode)): + lib_deps.add(lib_version) + if os.path.islink(dst_path): + os.unlink(dst_path) + else: + src_path = relative_path(lib_version[CONF_LOCAL]) + do_write = True + if os.path.islink(dst_path): + old_path = os.path.join(os.readlink(dst_path), lib_path) + if old_path != lib_path: + os.unlink(dst_path) + else: + do_write = False + if do_write: + mkdir_p(lib_path) + os.symlink(src_path, dst_path) + + # 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']) + lib_deps |= get_build_flags(config, 'LIB_DEPS') lib_deps |= get_build_flags(config, 'lib_deps') if core.ESP_PLATFORM == ESP_PLATFORM_ESP32: lib_deps |= { 'Preferences', # Preferences helper } + # Manual fix for AsyncTCP + if config[CONF_ESPHOMEYAML].get(CONF_ARDUINO_VERSION) == ARDUINO_VERSION_ESP32_DEV: + lib_deps.add('https://github.com/me-no-dev/AsyncTCP.git#idf-update') # avoid changing build flags order lib_deps = sorted(x for x in lib_deps if x) if lib_deps: @@ -178,7 +208,7 @@ def write_platformio_ini(content, path): def write_platformio_project(config, path): platformio_ini = os.path.join(path, 'platformio.ini') - content = get_ini_content(config) + content = get_ini_content(config, path) if 'esp32_ble_beacon' in config or 'esp32_ble_tracker' in config: content += 'board_build.partitions = partitions.csv\n' partitions_csv = os.path.join(path, 'partitions.csv') diff --git a/tests/test1.yaml b/tests/test1.yaml index 97adb73e93..d6d7652129 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2,10 +2,12 @@ esphomeyaml: name: test1 platform: ESP32 board: nodemcu-32s - # Use latest esphomelib git version. TODO: Change this - library_uri: 'https://github.com/OttoWinter/esphomelib.git' - simplify: false - use_build_flags: yes + # Use latest upstream esphomelib git version. + esphomelib_version: dev + # Use this for testing while developing: + # esphomelib_version: + # local: ~/path/to/esphomelib + use_custom_code: false on_boot: priority: 150.0 then: @@ -15,6 +17,10 @@ esphomeyaml: then: - lambda: >- ESP_LOGD("main", "ON SHUTDOWN!"); + on_loop: + then: + - lambda: >- + ESP_LOGV("main", "ON LOOP!"); build_path: build wifi: @@ -55,7 +61,7 @@ mqtt: qos: 0 then: - lambda: >- - ESP_LOGD("main", "Got message %s", x); + ESP_LOGD("main", "Got message %s", x.c_str()); - topic: livingroom/ota_mode then: - deep_sleep.prevent: @@ -905,6 +911,10 @@ time: - 0.pool.ntp.org - 1.pool.ntp.org - 2.pool.ntp.org + on_time: + cron: '/30 0-30,30/5 * ? JAN-DEC MON,SAT-SUN,TUE-FRI' + then: + - lambda: 'ESP_LOGD("main", "time");' cover: - platform: template