commit 982e9c10515ace8d63da9cc1114e14c94bb07665 Author: Otto Winter Date: Sat Apr 7 01:23:03 2018 +0200 Initial Commit 🎉 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..57a62c809e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,108 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.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 + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.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 +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +config/ +examples/ +Dockerfile diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..499c48478e --- /dev/null +++ b/.gitignore @@ -0,0 +1,106 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.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 + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.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 +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +config/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..52196f3460 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM python:2.7 +MAINTAINER Otto Winter + +ENV ESPHOMEYAML_OTA_HOST_PORT=6123 +EXPOSE 6123 +VOLUME /config +WORKDIR /usr/src/app + +COPY requirements.txt /usr/src/app/ +RUN pip install --no-cache-dir -r requirements.txt + +COPY docker/platformio.ini /usr/src/app/ +RUN platformio settings set enable_telemetry No && \ + platformio lib --global install esphomelib && \ + platformio run -e espressif32 -e espressif8266; exit 0 + +# Fix issue with static IP on ESP32: https://github.com/espressif/arduino-esp32/issues/1081 +RUN curl https://github.com/espressif/arduino-esp32/commit/144480637a718844b8f48f4392da8d4f622f2e5e.patch | \ + patch /root/.platformio/packages/framework-arduinoespressif32/libraries/WiFi/src/WiFiGeneric.cpp + +COPY . . +RUN pip install -e . + +WORKDIR /config +ENTRYPOINT ["esphomeyaml"] diff --git a/README.md b/README.md new file mode 100644 index 0000000000..6800bc39d4 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# esphomeyaml for [esphomelib](https://github.com/OttoWinter/esphomelib) + +Getting Started Guide: https://esphomlib.com/esphomeyaml/getting-started.html +Available Components: https://esphomelib.com/esphomeyaml/index.html + +esphomeyaml is the solution for your ESP8266/ESP32 projects with Home Assistant. It allows you to create **custom firmwares** for your microcontrollers with no programming experience required. All you need to know is the YAML configuration format which is also used by Home Assistant. + +esphomeyaml will: + + * Read your configuration file and warn you about potential errors (like using the invalid pins.) + * Create a custom C++ sketch file for you using esphomeyaml's powerful C++ generation engine. + * Compile the sketch file for you using [platformio](http://platformio.org/). + * Upload the binary to your ESP via Over the Air updates. + * Automatically start remote logs via MQTT. + +And all of that with a single command 🎉: + +```bash +esphomeyaml configuration.yaml run +``` + +## Features + + * **No programming experience required:** just edit YAML configuration + files like you're used to with Home Assistant. + * **Flexible:** Use [esphomelib](https://github.com/OttoWinter/esphomelib)'s powerful core to create custom sensors/outputs. + * **Fast and efficient:** Written in C++ and keeps memory consumption to a minimum. + * **Made for Home Assistant:** Almost all Home Assistant features are supported out of the box. Including RGB lights and many more. + * **Easy reproducible configuration:** No need to go through a long setup process for every single node. Just copy a configuration file and run a single command. + * **Smart Over The Air Updates:** esphomeyaml has OTA updates deeply integrated into the system. It even automatically enters a recovery mode if a boot loop is detected. + * **Powerful logging engine:** View colorful logs and debug issues remotely. + * **Open Source** + * For me: Makes documenting esphomelib's features a lot easier. + +## Special Thanks + +Special Thanks to the Home Assistant project. Lots of the code base of esphomeyaml is based off of Home Assistant, for example the loading and config validation code. diff --git a/docker/platformio.ini b/docker/platformio.ini new file mode 100644 index 0000000000..75a9f25987 --- /dev/null +++ b/docker/platformio.ini @@ -0,0 +1,12 @@ +; This file allows the docker build file to install the required platformio +; platforms + +[env:espressif32] +platform = espressif32 +board = nodemcu-32s +framework = arduino + +[env:espressif8266] +platform = espressif8266 +board = nodemcuv2 +framework = arduino diff --git a/esphomeyaml/__init__.py b/esphomeyaml/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphomeyaml/__main__.py b/esphomeyaml/__main__.py new file mode 100644 index 0000000000..d5abeb3b53 --- /dev/null +++ b/esphomeyaml/__main__.py @@ -0,0 +1,306 @@ +from __future__ import print_function + +import argparse +import logging +import os +import random +import sys + +from esphomeyaml import helpers, mqtt, writer, yaml_util, wizard +from esphomeyaml.config import add_component_task, read_config +from esphomeyaml.const import CONF_ESPHOMEYAML, CONF_HOSTNAME, CONF_MANUAL_IP, CONF_NAME, \ + CONF_STATIC_IP, \ + CONF_WIFI, CONF_LOGGER, CONF_BAUD_RATE +from esphomeyaml.helpers import AssignmentExpression, RawStatement, _EXPRESSIONS, add, \ + get_variable, indent, quote, statement + +_LOGGER = logging.getLogger(__name__) + +PRE_INITIALIZE = ['esphomeyaml', 'logger', 'wifi', 'ota', 'mqtt', 'i2c'] + +CONFIG_PATH = None + + +def get_name(config): + return config[CONF_ESPHOMEYAML][CONF_NAME] + + +def get_base_path(config): + return os.path.join(os.path.dirname(CONFIG_PATH), get_name(config)) + + +def discover_serial_ports(): + # from https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports.py + try: + from serial.tools.list_ports import comports + except ImportError: + return None + + result = None + for p, d, h in comports(): + if not p: + continue + if "VID:PID" in h: + if result is not None: + return None + result = p + + return result + + +def run_platformio(*cmd): + def mock_exit(rc): + raise SystemExit(rc) + + orig_argv = sys.argv + orig_exit = sys.exit # mock sys.exit + full_cmd = u' '.join(quote(x) for x in cmd) + _LOGGER.info(u"Running: %s", full_cmd) + try: + import platformio.__main__ + sys.argv = list(cmd) + sys.exit = mock_exit + return platformio.__main__.main() + except KeyboardInterrupt: + return 1 + except SystemExit as e: + return e.args[0] + except Exception as e: + _LOGGER.error(u"Running platformio failed: %s", e) + _LOGGER.error(u"Please try running %s locally.", full_cmd) + finally: + sys.argv = orig_argv + sys.exit = orig_exit + + +def run_miniterm(config, port): + from serial.tools import miniterm + baud_rate = config.get(CONF_LOGGER, {}).get(CONF_BAUD_RATE, 115200) + sys.argv = ['miniterm', '--raw', '--exit-char', '3'] + miniterm.main( + default_port=port, + default_baudrate=baud_rate) + + +def write_cpp(config): + _LOGGER.info("Generating C++ source...") + for domain in PRE_INITIALIZE: + if domain in config: + add_component_task(domain, config[domain]) + + # Clear queue + get_variable(None) + add(RawStatement('')) + + for domain, conf in config.iteritems(): + if domain in PRE_INITIALIZE: + continue + add_component_task(domain, conf) + + # Clear queue + get_variable(None) + add(RawStatement('')) + add(RawStatement('')) + + all_code = [] + for exp in _EXPRESSIONS: + if helpers.SIMPLIFY and isinstance(exp, AssignmentExpression) and exp.obj.usages == 0: + exp = exp.rhs + all_code.append(unicode(statement(exp))) + + platformio_ini_s = writer.get_ini_content(config) + ini_path = os.path.join(get_base_path(config), 'platformio.ini') + writer.write_platformio_ini(platformio_ini_s, ini_path) + + code_s = indent('\n'.join(all_code)) + cpp_path = os.path.join(get_base_path(config), 'src', 'main.cpp') + writer.write_cpp(code_s, cpp_path) + return 0 + + +def compile_program(config): + _LOGGER.info("Compiling app...") + return run_platformio('platformio', 'run', '-d', get_base_path(config)) + + +def upload_program(config, args, port): + _LOGGER.info("Uploading binary...") + if args.upload_port is not None: + if args.upload_port == 'HELLO': + return run_platformio('platformio', 'run', '-d', get_base_path(config), + '-t', 'upload') + else: + return run_platformio('platformio', 'run', '-d', get_base_path(config), + '-t', 'upload', '--upload-port', args.upload_port) + + if port is not None: + _LOGGER.info("Serial device discovered, using it for upload") + return run_platformio('platformio', 'run', '-d', get_base_path(config), + '-t', 'upload', '--upload-port', port) + + if CONF_MANUAL_IP in config[CONF_WIFI]: + host = str(config[CONF_WIFI][CONF_MANUAL_IP][CONF_STATIC_IP]) + elif CONF_HOSTNAME in config[CONF_WIFI]: + host = config[CONF_WIFI][CONF_HOSTNAME] + u'.local' + else: + host = config[CONF_ESPHOMEYAML][CONF_NAME] + u'.local' + + from esphomeyaml.components import ota + from esphomeyaml import espota + + bin_file = os.path.join(get_base_path(config), '.pioenvs', get_name(config), 'firmware.bin') + if args.host_port is not None: + host_port = args.host_port + else: + host_port = int(os.getenv('ESPHOMEYAML_OTA_HOST_PORT', random.randint(10000, 60000))) + espota_args = ['espota.py', '--debug', '--progress', '-i', host, + '-p', str(ota.get_port(config)), '-f', bin_file, + '-a', ota.get_auth(config), '-P', str(host_port)] + return espota.main(espota_args) + + +def show_logs(config, args, port): + if port is not None: + run_miniterm(config, port) + return 0 + return mqtt.show_logs(config, args.topic, args.username, args.password, args.client_id) + + +def clean_mqtt(config, args): + return mqtt.clear_topic(config, args.topic, args.username, args.password, args.client_id) + + +def setup_log(): + logging.basicConfig(level=logging.INFO) + fmt = "%(levelname)s [%(name)s] %(message)s" + colorfmt = "%(log_color)s{}%(reset)s".format(fmt) + datefmt = '%H:%M:%S' + + logging.getLogger('urllib3').setLevel(logging.WARNING) + + try: + from colorlog import ColoredFormatter + logging.getLogger().handlers[0].setFormatter(ColoredFormatter( + colorfmt, + datefmt=datefmt, + reset=True, + log_colors={ + 'DEBUG': 'cyan', + 'INFO': 'green', + 'WARNING': 'yellow', + 'ERROR': 'red', + 'CRITICAL': 'red', + } + )) + except ImportError: + pass + + +def main(): + global CONFIG_PATH + + setup_log() + + parser = argparse.ArgumentParser(prog='esphomeyaml') + parser.add_argument('configuration', help='Your YAML configuration file.') + subparsers = parser.add_subparsers(help='Commands', dest='command') + subparsers.required = True + parser_config = 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_upload = subparsers.add_parser('upload', help='Validate the configuration ' + 'and upload the latest binary.') + parser_upload.add_argument('--upload-port', help="Manually specify the upload port to use. " + "For example /dev/cu.SLAB_USBtoUAR.", + nargs='?', const='HELLO') + parser_upload.add_argument('--host-port', help="Specify the host port.", type=int) + + parser_logs = subparsers.add_parser('logs', help='Validate the configuration ' + 'and show all MQTT logs.') + parser_logs.add_argument('--topic', help='Manually set the topic to subscribe to.') + parser_logs.add_argument('--username', help='Manually set the username.') + parser_logs.add_argument('--password', help='Manually set the password.') + parser_logs.add_argument('--client-id', help='Manually set the client id.') + + parser_run = subparsers.add_parser('run', help='Validate the configuration, create a binary, ' + 'upload it, and start MQTT logs.') + parser_run.add_argument('--upload-port', help="Manually specify the upload port to use. " + "For example /dev/cu.SLAB_USBtoUAR.", + nargs='?', const='HELLO') + parser_run.add_argument('--host-port', help="Specify the host port to use for OTA", type=int) + parser_run.add_argument('--no-logs', help='Disable starting MQTT logs.', + action='store_true') + parser_run.add_argument('--topic', help='Manually set the topic to subscribe to for logs.') + parser_run.add_argument('--username', help='Manually set the MQTT username for logs.') + parser_run.add_argument('--password', help='Manually set the MQTT password for logs.') + parser_run.add_argument('--client-id', help='Manually set the client id for logs.') + + parser_clean = subparsers.add_parser('clean-mqtt', help="Helper to clear an MQTT topic from " + "retain messages.") + parser_clean.add_argument('--topic', help='Manually set the topic to subscribe to.') + parser_clean.add_argument('--username', help='Manually set the username.') + parser_clean.add_argument('--password', help='Manually set the password.') + parser_clean.add_argument('--client-id', help='Manually set the client id.') + + parser_wizard = subparsers.add_parser('wizard', help="A helpful setup wizard that will guide " + "you through setting up esphomeyaml.") + + args = parser.parse_args() + + if args.command == 'wizard': + return wizard.wizard(args.configuration) + + CONFIG_PATH = args.configuration + config = read_config(CONFIG_PATH) + if config is None: + return 1 + + if args.command == 'config': + print(yaml_util.dump(config)) + elif args.command == 'compile': + exit_code = write_cpp(config) + if exit_code != 0: + return exit_code + exit_code = compile_program(config) + if exit_code != 0: + return exit_code + _LOGGER.info(u"Successfully compiled program.") + return 0 + elif args.command == 'upload': + port = discover_serial_ports() + exit_code = upload_program(config, args, port) + if exit_code != 0: + return exit_code + _LOGGER.info(u"Successfully uploaded program.") + return 0 + elif args.command == 'logs': + port = discover_serial_ports() + return show_logs(config, args, port) + elif args.command == 'clean-mqtt': + return clean_mqtt(config, args) + elif args.command == 'run': + exit_code = write_cpp(config) + if exit_code != 0: + return exit_code + exit_code = compile_program(config) + if exit_code != 0: + return exit_code + _LOGGER.info(u"Successfully compiled program.") + if args.no_logs: + return + port = discover_serial_ports() + exit_code = upload_program(config, args, port) + if exit_code != 0: + return exit_code + _LOGGER.info(u"Successfully uploaded program.") + return show_logs(config, args, port) + else: + print(u"Unknown command {}".format(args.command)) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/esphomeyaml/components/__init__.py b/esphomeyaml/components/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphomeyaml/components/ads1115.py b/esphomeyaml/components/ads1115.py new file mode 100644 index 0000000000..af179f6a19 --- /dev/null +++ b/esphomeyaml/components/ads1115.py @@ -0,0 +1,37 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.const import CONF_ADDRESS, CONF_ID, CONF_RATE +from esphomeyaml.helpers import App, Pvariable, RawExpression, add, HexIntLiteral + +DEPENDENCIES = ['i2c'] + +ADS1115_COMPONENT_CLASS = 'sensor::ADS1115Component' + +RATES = { + 8: 'ADS1115_RATE_8', + 16: 'ADS1115_RATE_16', + 32: 'ADS1115_RATE_32', + 64: 'ADS1115_RATE_64', + 128: 'ADS1115_RATE_128', + 250: 'ADS1115_RATE_250', + 475: 'ADS1115_RATE_475', + 860: 'ADS1115_RATE_860', +} + +ADS1115_SCHEMA = vol.Schema({ + cv.GenerateID('ads1115'): cv.register_variable_id, + vol.Required(CONF_ADDRESS): cv.i2c_address, + vol.Optional(CONF_RATE): vol.All(vol.Coerce(int), vol.Any(*list(RATES.keys()))), +}) + +CONFIG_SCHEMA = vol.All(cv.ensure_list, [ADS1115_SCHEMA]) + + +def to_code(config): + for conf in config: + address = HexIntLiteral(conf[CONF_ADDRESS]) + rhs = App.make_ads1115_component(address) + ads1115 = Pvariable(ADS1115_COMPONENT_CLASS, conf[CONF_ID], rhs) + if CONF_RATE in conf: + add(ads1115.set_rate(RawExpression(RATES[conf[CONF_RATE]]))) diff --git a/esphomeyaml/components/binary_sensor/__init__.py b/esphomeyaml/components/binary_sensor/__init__.py new file mode 100644 index 0000000000..b6dae25013 --- /dev/null +++ b/esphomeyaml/components/binary_sensor/__init__.py @@ -0,0 +1,29 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.const import CONF_DEVICE_CLASS, CONF_INVERTED +from esphomeyaml.helpers import add, setup_mqtt_component + +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_INVERTED): cv.boolean, +}) + +DEVICE_CLASSES = [ + '', 'battery', 'cold', 'connectivity', 'door', 'garage_door', 'gas', + 'heat', 'light', 'lock', 'moisture', 'motion', 'moving', 'occupancy', + 'opening', 'plug', 'power', 'presence', 'problem', 'safety', 'smoke', + 'sound', 'vibration', 'window' +] + +DEVICE_CLASSES_MSG = "Unknown device class. Must be one of {}".format(', '.join(DEVICE_CLASSES)) + +MQTT_BINARY_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend({ + vol.Optional(CONF_DEVICE_CLASS): vol.All(vol.Lower, + vol.Any(*DEVICE_CLASSES, msg=DEVICE_CLASSES_MSG)), +}) + + +def setup_mqtt_binary_sensor(obj, config, skip_device_class=False): + if not skip_device_class and CONF_DEVICE_CLASS in config: + add(obj.set_device_class(config[CONF_DEVICE_CLASS])) + setup_mqtt_component(obj, config) diff --git a/esphomeyaml/components/binary_sensor/gpio.py b/esphomeyaml/components/binary_sensor/gpio.py new file mode 100644 index 0000000000..2dda302204 --- /dev/null +++ b/esphomeyaml/components/binary_sensor/gpio.py @@ -0,0 +1,21 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml import pins +from esphomeyaml.components import binary_sensor +from esphomeyaml.const import CONF_DEVICE_CLASS, CONF_ID, CONF_INVERTED, CONF_NAME, CONF_PIN +from esphomeyaml.helpers import App, add, exp_gpio_input_pin, variable + +PLATFORM_SCHEMA = binary_sensor.PLATFORM_SCHEMA.extend({ + cv.GenerateID('gpio_binary_sensor'): cv.register_variable_id, + vol.Required(CONF_PIN): pins.GPIO_INPUT_PIN_SCHEMA +}).extend(binary_sensor.MQTT_BINARY_SENSOR_SCHEMA.schema) + + +def to_code(config): + rhs = App.make_gpio_binary_sensor(exp_gpio_input_pin(config[CONF_PIN]), + config[CONF_NAME], config.get(CONF_DEVICE_CLASS)) + gpio = variable('Application::SimpleBinarySensor', config[CONF_ID], rhs) + if CONF_INVERTED in config: + add(gpio.Pgpio.set_inverted(config[CONF_INVERTED])) + binary_sensor.setup_mqtt_binary_sensor(gpio.Pmqtt, config, skip_device_class=True) diff --git a/esphomeyaml/components/binary_sensor/status.py b/esphomeyaml/components/binary_sensor/status.py new file mode 100644 index 0000000000..bd6685b74b --- /dev/null +++ b/esphomeyaml/components/binary_sensor/status.py @@ -0,0 +1,14 @@ +import esphomeyaml.config_validation as cv +from esphomeyaml.components import binary_sensor +from esphomeyaml.const import CONF_ID, CONF_NAME +from esphomeyaml.helpers import App, Pvariable + +PLATFORM_SCHEMA = binary_sensor.PLATFORM_SCHEMA.extend({ + cv.GenerateID('status_binary_sensor'): cv.register_variable_id, +}).extend(binary_sensor.MQTT_BINARY_SENSOR_SCHEMA.schema) + + +def to_code(config): + rhs = App.make_status_binary_sensor(config[CONF_NAME]) + gpio = Pvariable('binary_sensor::MQTTBinarySensorComponent', config[CONF_ID], rhs) + binary_sensor.setup_mqtt_binary_sensor(gpio.Pmqtt, config) diff --git a/esphomeyaml/components/dallas.py b/esphomeyaml/components/dallas.py new file mode 100644 index 0000000000..87852104e2 --- /dev/null +++ b/esphomeyaml/components/dallas.py @@ -0,0 +1,20 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml import pins +from esphomeyaml.const import CONF_ID, CONF_PIN, CONF_UPDATE_INTERVAL +from esphomeyaml.helpers import App, Pvariable + +DALLAS_COMPONENT_CLASS = 'sensor::DallasComponent' + +CONFIG_SCHEMA = vol.All(cv.ensure_list, [vol.Schema({ + cv.GenerateID('dallas'): cv.register_variable_id, + vol.Required(CONF_PIN): pins.input_output_pin, + vol.Optional(CONF_UPDATE_INTERVAL): cv.positive_not_null_time_period, +})]) + + +def to_code(config): + for conf in config: + rhs = App.make_dallas_component(conf[CONF_PIN], conf.get(CONF_UPDATE_INTERVAL)) + Pvariable(DALLAS_COMPONENT_CLASS, conf[CONF_ID], rhs) diff --git a/esphomeyaml/components/fan/__init__.py b/esphomeyaml/components/fan/__init__.py new file mode 100644 index 0000000000..6237857f25 --- /dev/null +++ b/esphomeyaml/components/fan/__init__.py @@ -0,0 +1,23 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.const import CONF_OSCILLATION_COMMAND_TOPIC, CONF_OSCILLATION_STATE_TOPIC, \ + CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC +from esphomeyaml.helpers import add, setup_mqtt_component + +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_OSCILLATION_STATE_TOPIC): cv.publish_topic, + vol.Optional(CONF_OSCILLATION_COMMAND_TOPIC): cv.subscribe_topic, +}).extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA.schema) + + +def setup_mqtt_fan(obj, config): + if CONF_OSCILLATION_STATE_TOPIC in config: + add(obj.set_custom_oscillation_state_topic(config[CONF_OSCILLATION_STATE_TOPIC])) + if CONF_OSCILLATION_COMMAND_TOPIC in config: + add(obj.set_custom_oscillation_command_topic(config[CONF_OSCILLATION_COMMAND_TOPIC])) + if CONF_SPEED_STATE_TOPIC in config: + add(obj.set_custom_speed_state_topic(config[CONF_SPEED_STATE_TOPIC])) + if CONF_SPEED_COMMAND_TOPIC in config: + add(obj.set_custom_speed_command_topic(config[CONF_SPEED_COMMAND_TOPIC])) + setup_mqtt_component(obj, config) diff --git a/esphomeyaml/components/fan/binary.py b/esphomeyaml/components/fan/binary.py new file mode 100644 index 0000000000..044bef557d --- /dev/null +++ b/esphomeyaml/components/fan/binary.py @@ -0,0 +1,23 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.components import fan +from esphomeyaml.const import CONF_ID, CONF_NAME, CONF_OSCILLATION_OUTPUT, CONF_OUTPUT +from esphomeyaml.helpers import App, add, get_variable, variable + +PLATFORM_SCHEMA = fan.PLATFORM_SCHEMA.extend({ + cv.GenerateID('binary_fan'): cv.register_variable_id, + vol.Required(CONF_OUTPUT): cv.variable_id, + vol.Optional(CONF_OSCILLATION_OUTPUT): cv.variable_id, +}) + + +def to_code(config): + output = get_variable(config[CONF_OUTPUT]) + rhs = App.make_fan(config[CONF_NAME]) + fan_struct = variable('Application::FanStruct', config[CONF_ID], rhs) + add(fan_struct.Poutput.set_binary(output)) + if CONF_OSCILLATION_OUTPUT in config: + oscillation_output = get_variable(config[CONF_OSCILLATION_OUTPUT]) + add(fan_struct.Poutput.set_oscillation(oscillation_output)) + fan.setup_mqtt_fan(fan_struct.Pmqtt, config) diff --git a/esphomeyaml/components/fan/speed.py b/esphomeyaml/components/fan/speed.py new file mode 100644 index 0000000000..1b18cba13f --- /dev/null +++ b/esphomeyaml/components/fan/speed.py @@ -0,0 +1,40 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.components import fan +from esphomeyaml.const import CONF_HIGH, CONF_ID, CONF_LOW, \ + CONF_MEDIUM, CONF_NAME, CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, CONF_SPEED, \ + CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC +from esphomeyaml.helpers import App, add, get_variable, variable + +PLATFORM_SCHEMA = fan.PLATFORM_SCHEMA.extend({ + cv.GenerateID('speed_fan'): cv.register_variable_id, + vol.Required(CONF_OUTPUT): cv.variable_id, + vol.Optional(CONF_SPEED_STATE_TOPIC): cv.publish_topic, + vol.Optional(CONF_SPEED_COMMAND_TOPIC): cv.subscribe_topic, + vol.Optional(CONF_OSCILLATION_OUTPUT): cv.variable_id, + vol.Optional(CONF_SPEED): vol.Schema({ + vol.Required(CONF_LOW): cv.zero_to_one_float, + vol.Required(CONF_MEDIUM): cv.zero_to_one_float, + vol.Required(CONF_HIGH): cv.zero_to_one_float, + }), +}) + + +def to_code(config): + output = get_variable(config[CONF_OUTPUT]) + rhs = App.make_fan(config[CONF_NAME]) + fan_struct = variable('Application::FanStruct', config[CONF_ID], rhs) + if CONF_SPEED in config: + speeds = config[CONF_SPEED] + add(fan_struct.Poutput.set_speed(output, 0.0, + speeds[CONF_LOW], + speeds[CONF_MEDIUM], + speeds[CONF_HIGH])) + else: + add(fan_struct.Poutput.set_speed(output)) + + if CONF_OSCILLATION_OUTPUT in config: + oscillation_output = get_variable(config[CONF_OSCILLATION_OUTPUT]) + add(fan_struct.Poutput.set_oscillation(oscillation_output)) + fan.setup_mqtt_fan(fan_struct.Pmqtt, config) diff --git a/esphomeyaml/components/i2c.py b/esphomeyaml/components/i2c.py new file mode 100644 index 0000000000..a052245cd3 --- /dev/null +++ b/esphomeyaml/components/i2c.py @@ -0,0 +1,16 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml import pins +from esphomeyaml.const import CONF_FREQUENCY, CONF_SCL, CONF_SDA +from esphomeyaml.helpers import App, add + +CONFIG_SCHEMA = vol.Schema({ + vol.Required(CONF_SDA, default='SDA'): pins.input_output_pin, + vol.Required(CONF_SCL, default='SCL'): pins.input_output_pin, + vol.Optional(CONF_FREQUENCY): vol.All(cv.only_on_esp32, cv.positive_int), +}) + + +def to_code(config): + add(App.init_i2c(config[CONF_SDA], config[CONF_SCL], config.get(CONF_FREQUENCY))) diff --git a/esphomeyaml/components/ir_transmitter.py b/esphomeyaml/components/ir_transmitter.py new file mode 100644 index 0000000000..d20a35f69a --- /dev/null +++ b/esphomeyaml/components/ir_transmitter.py @@ -0,0 +1,23 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml import pins +from esphomeyaml.const import CONF_CARRIER_DUTY_PERCENT, CONF_ID, CONF_PIN, ESP_PLATFORM_ESP32 +from esphomeyaml.helpers import App, Pvariable, exp_gpio_output_pin + +ESP_PLATFORMS = [ESP_PLATFORM_ESP32] + +IR_TRANSMITTER_COMPONENT_CLASS = 'switch_::IRTransmitterComponent' + +CONFIG_SCHEMA = vol.All(cv.ensure_list, [vol.Schema({ + cv.GenerateID('ir_transmitter'): cv.register_variable_id, + vol.Required(CONF_PIN): pins.GPIO_OUTPUT_PIN_SCHEMA, + vol.Optional(CONF_CARRIER_DUTY_PERCENT): vol.All(vol.Coerce(int), vol.Range(min=0, max=100)), +})]) + + +def to_code(config): + for conf in config: + pin = exp_gpio_output_pin(conf[CONF_PIN]) + rhs = App.make_ir_transmitter(pin, conf.get(CONF_CARRIER_DUTY_PERCENT)) + Pvariable(IR_TRANSMITTER_COMPONENT_CLASS, conf[CONF_ID], rhs) diff --git a/esphomeyaml/components/light/__init__.py b/esphomeyaml/components/light/__init__.py new file mode 100644 index 0000000000..ec0b02ba75 --- /dev/null +++ b/esphomeyaml/components/light/__init__.py @@ -0,0 +1,13 @@ +import esphomeyaml.config_validation as cv +from esphomeyaml.const import CONF_DEFAULT_TRANSITION_LENGTH +from esphomeyaml.helpers import add, setup_mqtt_component + +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ + +}).extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA.schema) + + +def setup_mqtt_light_component(obj, config): + if CONF_DEFAULT_TRANSITION_LENGTH in config: + add(obj.set_default_transition_length(config[CONF_DEFAULT_TRANSITION_LENGTH])) + setup_mqtt_component(obj, config) diff --git a/esphomeyaml/components/light/binary.py b/esphomeyaml/components/light/binary.py new file mode 100644 index 0000000000..b8688f9cd7 --- /dev/null +++ b/esphomeyaml/components/light/binary.py @@ -0,0 +1,19 @@ + +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.components import light +from esphomeyaml.const import CONF_ID, CONF_NAME, CONF_OUTPUT +from esphomeyaml.helpers import App, get_variable, variable + +PLATFORM_SCHEMA = light.PLATFORM_SCHEMA.extend({ + cv.GenerateID('binary_light'): cv.register_variable_id, + vol.Required(CONF_OUTPUT): cv.variable_id, +}) + + +def to_code(config): + output = get_variable(config[CONF_OUTPUT]) + rhs = App.make_binary_light(config[CONF_NAME], output) + light_struct = variable('Application::LightStruct', config[CONF_ID], rhs) + light.setup_mqtt_light_component(light_struct.Pmqtt, config) diff --git a/esphomeyaml/components/light/monochromatic.py b/esphomeyaml/components/light/monochromatic.py new file mode 100644 index 0000000000..e9dce19b46 --- /dev/null +++ b/esphomeyaml/components/light/monochromatic.py @@ -0,0 +1,23 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.components import light +from esphomeyaml.const import CONF_DEFAULT_TRANSITION_LENGTH, CONF_GAMMA_CORRECT, CONF_ID, \ + CONF_NAME, CONF_OUTPUT +from esphomeyaml.helpers import App, add, get_variable, variable + +PLATFORM_SCHEMA = light.PLATFORM_SCHEMA.extend({ + cv.GenerateID('monochromatic_light'): cv.register_variable_id, + vol.Required(CONF_OUTPUT): cv.variable_id, + vol.Optional(CONF_GAMMA_CORRECT): cv.positive_float, + vol.Optional(CONF_DEFAULT_TRANSITION_LENGTH): cv.positive_time_period, +}) + + +def to_code(config): + output = get_variable(config[CONF_OUTPUT]) + rhs = App.make_monochromatic_light(config[CONF_NAME], output) + light_struct = variable('Application::LightStruct', config[CONF_ID], rhs) + if CONF_GAMMA_CORRECT in config: + add(light_struct.Poutput.set_gamma_correct(config[CONF_GAMMA_CORRECT])) + light.setup_mqtt_light_component(light_struct.Pmqtt, config) diff --git a/esphomeyaml/components/light/rgb.py b/esphomeyaml/components/light/rgb.py new file mode 100644 index 0000000000..eb8496bb5c --- /dev/null +++ b/esphomeyaml/components/light/rgb.py @@ -0,0 +1,27 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.components import light +from esphomeyaml.const import CONF_BLUE, CONF_DEFAULT_TRANSITION_LENGTH, CONF_GAMMA_CORRECT, \ + CONF_GREEN, CONF_ID, CONF_NAME, CONF_RED +from esphomeyaml.helpers import App, add, get_variable, variable + +PLATFORM_SCHEMA = light.PLATFORM_SCHEMA.extend({ + cv.GenerateID('rgb_light'): cv.register_variable_id, + vol.Required(CONF_RED): cv.variable_id, + vol.Required(CONF_GREEN): cv.variable_id, + vol.Required(CONF_BLUE): cv.variable_id, + vol.Optional(CONF_GAMMA_CORRECT): cv.positive_float, + vol.Optional(CONF_DEFAULT_TRANSITION_LENGTH): cv.positive_time_period, +}) + + +def to_code(config): + red = get_variable(config[CONF_RED]) + green = get_variable(config[CONF_GREEN]) + blue = get_variable(config[CONF_BLUE]) + rhs = App.make_rgb_light(config[CONF_NAME], red, green, blue) + light_struct = variable('Application::LightStruct', config[CONF_ID], rhs) + if CONF_GAMMA_CORRECT in config: + add(light_struct.Poutput.set_gamma_correct(config[CONF_GAMMA_CORRECT])) + light.setup_mqtt_light_component(light_struct.Pmqtt, config) diff --git a/esphomeyaml/components/light/rgbw.py b/esphomeyaml/components/light/rgbw.py new file mode 100644 index 0000000000..4a2247adf6 --- /dev/null +++ b/esphomeyaml/components/light/rgbw.py @@ -0,0 +1,29 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.components import light +from esphomeyaml.const import CONF_BLUE, CONF_DEFAULT_TRANSITION_LENGTH, CONF_GAMMA_CORRECT, \ + CONF_GREEN, CONF_ID, CONF_NAME, CONF_RED, CONF_WHITE +from esphomeyaml.helpers import App, get_variable, variable, add + +PLATFORM_SCHEMA = light.PLATFORM_SCHEMA.extend({ + cv.GenerateID('rgbw_light'): cv.register_variable_id, + vol.Required(CONF_RED): cv.variable_id, + vol.Required(CONF_GREEN): cv.variable_id, + vol.Required(CONF_BLUE): cv.variable_id, + vol.Required(CONF_WHITE): cv.variable_id, + vol.Optional(CONF_GAMMA_CORRECT): cv.positive_float, + vol.Optional(CONF_DEFAULT_TRANSITION_LENGTH): cv.positive_time_period, +}) + + +def to_code(config): + red = get_variable(config[CONF_RED]) + green = get_variable(config[CONF_GREEN]) + blue = get_variable(config[CONF_BLUE]) + white = get_variable(config[CONF_WHITE]) + rhs = App.make_rgbw_light(config[CONF_NAME], red, green, blue, white) + light_struct = variable('Application::LightStruct', config[CONF_ID], rhs) + if CONF_GAMMA_CORRECT in config: + add(light_struct.Poutput.set_gamma_correct(config[CONF_GAMMA_CORRECT])) + light.setup_mqtt_light_component(light_struct.Pmqtt, config) diff --git a/esphomeyaml/components/logger.py b/esphomeyaml/components/logger.py new file mode 100644 index 0000000000..8738f0d0c1 --- /dev/null +++ b/esphomeyaml/components/logger.py @@ -0,0 +1,60 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.const import CONF_BAUD_RATE, CONF_ID, CONF_LEVEL, CONF_LOGGER, CONF_LOGS, \ + CONF_LOG_TOPIC, CONF_TX_BUFFER_SIZE +from esphomeyaml.core import ESPHomeYAMLError +from esphomeyaml.helpers import App, Pvariable, RawExpression, add, exp_empty_optional + +LOG_LEVELS = ['NONE', 'ERROR', 'WARN', 'INFO', 'DEBUG', 'VERBOSE'] + +is_log_level = vol.All(vol.Upper, vol.Any(*LOG_LEVELS)) + +CONFIG_SCHEMA = cv.ID_SCHEMA.extend({ + cv.GenerateID(CONF_LOGGER): cv.register_variable_id, + vol.Optional(CONF_BAUD_RATE): cv.positive_int, + vol.Optional(CONF_LOG_TOPIC): vol.Any(None, '', cv.publish_topic), + vol.Optional(CONF_TX_BUFFER_SIZE): cv.positive_int, + vol.Optional(CONF_LEVEL): is_log_level, + vol.Optional(CONF_LOGS): vol.Schema({ + cv.string: is_log_level, + }) +}) + + +def esphomelib_log_level(level): + return u'ESPHOMELIB_LOG_LEVEL_{}'.format(level) + + +def exp_log_level(level): + return RawExpression(esphomelib_log_level(level)) + + +def to_code(config): + baud_rate = config.get(CONF_BAUD_RATE) + if baud_rate is None and CONF_LOG_TOPIC in config: + baud_rate = 115200 + log_topic = None + if CONF_LOG_TOPIC in config: + if not config[CONF_LOG_TOPIC]: + log_topic = exp_empty_optional(u'std::string') + else: + log_topic = config[CONF_LOG_TOPIC] + rhs = App.init_log(baud_rate, log_topic) + log = Pvariable(u'LogComponent', config[CONF_ID], rhs) + if CONF_TX_BUFFER_SIZE in config: + add(log.set_tx_buffer_size(config[CONF_TX_BUFFER_SIZE])) + if CONF_LEVEL in config: + add(log.set_global_log_level(exp_log_level(config[CONF_LEVEL]))) + for tag, level in config.get(CONF_LOGS, {}).iteritems(): + global_level = config.get(CONF_LEVEL, 'DEBUG') + if LOG_LEVELS.index(level) > LOG_LEVELS.index(global_level): + raise ESPHomeYAMLError(u"The local log level {} for {} must be less severe than the " + u"global log level {}.".format(level, tag, global_level)) + add(log.set_log_level(tag, exp_log_level(level))) + + +def get_build_flags(config): + if CONF_LEVEL in config: + return u'-DESPHOMELIB_LOG_LEVEL={}'.format(esphomelib_log_level(config[CONF_LEVEL])) + return u'' diff --git a/esphomeyaml/components/mqtt.py b/esphomeyaml/components/mqtt.py new file mode 100644 index 0000000000..7fc0ea9200 --- /dev/null +++ b/esphomeyaml/components/mqtt.py @@ -0,0 +1,74 @@ +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 +from esphomeyaml.helpers import App, Pvariable, StructInitializer, add, exp_empty_optional + +MQTT_WILL_BIRTH_SCHEMA = vol.Any(None, vol.Schema({ + vol.Required(CONF_TOPIC): cv.publish_topic, + vol.Required(CONF_PAYLOAD): cv.mqtt_payload, + vol.Optional(CONF_QOS, default=0): vol.All(vol.Coerce(int), vol.In([0, 1, 2])), + vol.Optional(CONF_RETAIN, default=True): cv.boolean, +})) + + +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.") + if u':' in value: + raise vol.Invalid(u"Please specify the port using the port: option") + return value + + +CONFIG_SCHEMA = cv.ID_SCHEMA.extend({ + cv.GenerateID(CONF_MQTT): cv.register_variable_id, + vol.Required(CONF_BROKER): validate_broker, + vol.Optional(CONF_PORT, default=1883): cv.port, + vol.Optional(CONF_USERNAME, default=''): cv.string, + vol.Optional(CONF_PASSWORD, default=''): cv.string, + vol.Optional(CONF_CLIENT_ID): vol.All(cv.string, vol.Length(max=23)), + vol.Optional(CONF_DISCOVERY): cv.boolean, + vol.Optional(CONF_DISCOVERY_RETAIN): cv.boolean, + vol.Optional(CONF_DISCOVERY_PREFIX): cv.publish_topic, + vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_TOPIC_PREFIX): cv.publish_topic, +}) + + +def exp_mqtt_message(config): + if config is None: + return exp_empty_optional('mqtt::MQTTMessage') + exp = StructInitializer( + 'mqtt::MQTTMessage', + ('topic', config[CONF_TOPIC]), + ('payload', config[CONF_PAYLOAD]), + ('qos', config[CONF_QOS]), + ('retain', config[CONF_RETAIN]) + ) + return exp + + +def to_code(config): + rhs = App.init_mqtt(config[CONF_BROKER], config[CONF_PORT], + config[CONF_USERNAME], config[CONF_PASSWORD]) + mqtt = Pvariable('mqtt::MQTTClientComponent', config[CONF_ID], rhs) + if not config.get(CONF_DISCOVERY, True): + add(mqtt.disable_discovery()) + if CONF_DISCOVERY_RETAIN in config or CONF_DISCOVERY_PREFIX in config: + discovery_retain = config.get(CONF_DISCOVERY_RETAIN, True) + discovery_prefix = config.get(CONF_DISCOVERY_PREFIX, 'homeassistant') + add(mqtt.set_discovery_info(discovery_prefix, discovery_retain)) + if CONF_BIRTH_MESSAGE in config: + add(mqtt.set_birth_message(config[CONF_BIRTH_MESSAGE])) + if CONF_WILL_MESSAGE in config: + add(mqtt.set_last_will(config[CONF_WILL_MESSAGE])) + if CONF_TOPIC_PREFIX in config: + add(mqtt.set_topic_prefix(config[CONF_TOPIC_PREFIX])) + if CONF_CLIENT_ID in config: + add(mqtt.set_client_id(config[CONF_CLIENT_ID])) diff --git a/esphomeyaml/components/ota.py b/esphomeyaml/components/ota.py new file mode 100644 index 0000000000..721896b0b2 --- /dev/null +++ b/esphomeyaml/components/ota.py @@ -0,0 +1,44 @@ +import hashlib +import logging + +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.const import CONF_ID, CONF_OTA, CONF_PASSWORD, CONF_PORT, CONF_SAFE_MODE, \ + ESP_PLATFORM_ESP8266, ESP_PLATFORM_ESP32 +from esphomeyaml.core import ESPHomeYAMLError +from esphomeyaml.helpers import App, Pvariable, add + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = cv.ID_SCHEMA.extend({ + cv.GenerateID(CONF_OTA): cv.register_variable_id, + vol.Optional(CONF_SAFE_MODE, default=True): cv.boolean, + # TODO Num attempts + wait time + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_PASSWORD): cv.string, +}) + + +def to_code(config): + rhs = App.init_ota() + ota = Pvariable('OTAComponent', config[CONF_ID], rhs) + if CONF_PASSWORD in config: + h = hashlib.md5(config[CONF_PASSWORD].encode()).hexdigest() + add(ota.set_auth_password_hash(h)) + if config[CONF_SAFE_MODE]: + add(ota.start_safe_mode()) + + +def get_port(config): + if CONF_PORT in config[CONF_OTA]: + return config[CONF_OTA][CONF_PORT] + if cv.ESP_PLATFORM == ESP_PLATFORM_ESP32: + return 3232 + elif cv.ESP_PLATFORM == ESP_PLATFORM_ESP8266: + return 8266 + raise ESPHomeYAMLError(u"Invalid ESP Platform for ESP OTA port.") + + +def get_auth(config): + return config[CONF_OTA].get(CONF_PASSWORD, '') diff --git a/esphomeyaml/components/output/__init__.py b/esphomeyaml/components/output/__init__.py new file mode 100644 index 0000000000..ff6281191a --- /dev/null +++ b/esphomeyaml/components/output/__init__.py @@ -0,0 +1,25 @@ + +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.const import CONF_POWER_SUPPLY, CONF_INVERTED, CONF_MAX_POWER +from esphomeyaml.helpers import get_variable, add + +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_POWER_SUPPLY): cv.variable_id, + vol.Optional(CONF_INVERTED): cv.boolean, +}).extend(cv.REQUIRED_ID_SCHEMA.schema) + +FLOAT_PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MAX_POWER): cv.zero_to_one_float, +}) + + +def setup_output_platform(obj, config, skip_power_supply=False): + if CONF_INVERTED in config: + add(obj.set_inverted(config[CONF_INVERTED])) + if not skip_power_supply and CONF_POWER_SUPPLY in config: + power_supply = get_variable(config[CONF_POWER_SUPPLY]) + add(obj.set_power_supply(power_supply)) + if CONF_MAX_POWER in config: + add(obj.set_max_power(config[CONF_MAX_POWER])) diff --git a/esphomeyaml/components/output/esp8266_pwm.py b/esphomeyaml/components/output/esp8266_pwm.py new file mode 100644 index 0000000000..d4899dfe3d --- /dev/null +++ b/esphomeyaml/components/output/esp8266_pwm.py @@ -0,0 +1,24 @@ +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.core import ESPHomeYAMLError +from esphomeyaml.helpers import App, Pvariable, exp_gpio_output_pin, get_gpio_pin_number + +ESP_PLATFORMS = [ESP_PLATFORM_ESP8266] + +PLATFORM_SCHEMA = output.FLOAT_PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PIN): pins.GPIO_OUTPUT_PIN_SCHEMA, +}) + + +def to_code(config): + if get_gpio_pin_number(config[CONF_PIN]) >= 16: + # Too difficult to do in config validation + raise ESPHomeYAMLError(u"ESP8266: Only pins 0-16 support PWM.") + pin = exp_gpio_output_pin(config[CONF_PIN]) + rhs = App.make_esp8266_pwm_output(pin) + gpio = Pvariable('output::ESP8266PWMOutput', config[CONF_ID], rhs) + output.setup_output_platform(gpio, config) diff --git a/esphomeyaml/components/output/gpio.py b/esphomeyaml/components/output/gpio.py new file mode 100644 index 0000000000..47e9dc22d0 --- /dev/null +++ b/esphomeyaml/components/output/gpio.py @@ -0,0 +1,17 @@ +import voluptuous as vol + +from esphomeyaml import pins +from esphomeyaml.components import output +from esphomeyaml.const import CONF_ID, CONF_PIN +from esphomeyaml.helpers import App, Pvariable, exp_gpio_output_pin + +PLATFORM_SCHEMA = output.PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PIN): pins.GPIO_OUTPUT_PIN_SCHEMA, +}) + + +def to_code(config): + pin = exp_gpio_output_pin(config[CONF_PIN]) + rhs = App.make_gpio_output(pin) + gpio = Pvariable('output::GPIOBinaryOutputComponent', config[CONF_ID], rhs) + output.setup_output_platform(gpio, config) diff --git a/esphomeyaml/components/output/ledc.py b/esphomeyaml/components/output/ledc.py new file mode 100644 index 0000000000..f36b8478fa --- /dev/null +++ b/esphomeyaml/components/output/ledc.py @@ -0,0 +1,38 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml import pins +from esphomeyaml.components import output +from esphomeyaml.const import APB_CLOCK_FREQ, CONF_BIT_DEPTH, CONF_CHANNEL, CONF_FREQUENCY, \ + CONF_ID, CONF_PIN, ESP_PLATFORM_ESP32 +from esphomeyaml.helpers import App, Pvariable, add + +ESP_PLATFORMS = [ESP_PLATFORM_ESP32] + + +def validate_frequency_bit_depth(obj): + frequency = obj.get(CONF_FREQUENCY, 1000) + bit_depth = obj.get(CONF_BIT_DEPTH, 12) + max_freq = APB_CLOCK_FREQ / (2**bit_depth) + if frequency > max_freq: + raise vol.Invalid('Maximum frequency for bit depth {} is {}'.format(bit_depth, max_freq)) + return obj + + +PLATFORM_SCHEMA = vol.All(output.FLOAT_PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PIN): vol.All(pins.output_pin, vol.Range(min=0, max=33)), + vol.Optional(CONF_FREQUENCY): cv.frequency, + vol.Optional(CONF_BIT_DEPTH): vol.All(vol.Coerce(int), vol.Range(min=1, max=15)), + vol.Optional(CONF_CHANNEL): vol.All(vol.Coerce(int), vol.Range(min=0, max=15)) +}), validate_frequency_bit_depth) + + +def to_code(config): + frequency = config.get(CONF_FREQUENCY) + if frequency is None and CONF_BIT_DEPTH in config: + frequency = 1000 + rhs = App.make_ledc_output(config[CONF_PIN], frequency, config.get(CONF_BIT_DEPTH)) + ledc = Pvariable('output::LEDCOutputComponent', config[CONF_ID], rhs) + if CONF_CHANNEL in config: + add(ledc.set_channel(config[CONF_CHANNEL])) + output.setup_output_platform(ledc, config) diff --git a/esphomeyaml/components/output/pca9685.py b/esphomeyaml/components/output/pca9685.py new file mode 100644 index 0000000000..66207267e2 --- /dev/null +++ b/esphomeyaml/components/output/pca9685.py @@ -0,0 +1,25 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.components import output +from esphomeyaml.components.pca9685 import PCA9685_COMPONENT_TYPE +from esphomeyaml.const import CONF_CHANNEL, CONF_ID, CONF_PCA9685_ID, CONF_POWER_SUPPLY +from esphomeyaml.helpers import Pvariable, get_variable + +DEPENDENCIES = ['pca9685'] + +PLATFORM_SCHEMA = output.FLOAT_PLATFORM_SCHEMA.extend({ + vol.Required(CONF_CHANNEL): vol.All(vol.Coerce(int), + vol.Range(min=0, max=15)), + vol.Optional(CONF_PCA9685_ID): cv.variable_id, +}) + + +def to_code(config): + power_supply = None + if CONF_POWER_SUPPLY in config: + power_supply = get_variable(config[CONF_POWER_SUPPLY]) + pca9685 = get_variable(config.get(CONF_PCA9685_ID), PCA9685_COMPONENT_TYPE) + rhs = pca9685.create_channel(config[CONF_CHANNEL], power_supply) + out = Pvariable('output::PCA9685OutputComponent::Channel', config[CONF_ID], rhs) + output.setup_output_platform(out, config, skip_power_supply=True) diff --git a/esphomeyaml/components/pca9685.py b/esphomeyaml/components/pca9685.py new file mode 100644 index 0000000000..fb8e6cc428 --- /dev/null +++ b/esphomeyaml/components/pca9685.py @@ -0,0 +1,33 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.const import CONF_ADDRESS, CONF_FREQUENCY, CONF_ID, CONF_PHASE_BALANCER +from esphomeyaml.helpers import App, HexIntLiteral, Pvariable, RawExpression, add + +DEPENDENCIES = ['i2c'] + +PHASE_BALANCERS = ['None', 'Linear', 'Weaved'] + +PCA9685_COMPONENT_TYPE = 'output::PCA9685OutputComponent' + +PCA9685_SCHEMA = vol.Schema({ + cv.GenerateID('pca9685'): cv.register_variable_id, + vol.Required(CONF_FREQUENCY): vol.All(cv.frequency, + vol.Range(min=24, max=1526)), + vol.Optional(CONF_PHASE_BALANCER): vol.All(vol.Title, vol.Any(*PHASE_BALANCERS)), + vol.Optional(CONF_ADDRESS): cv.i2c_address, +}) + +CONFIG_SCHEMA = vol.All(cv.ensure_list, [PCA9685_SCHEMA]) + + +def to_code(config): + for conf in config: + rhs = App.make_pca9685_component(conf.get(CONF_FREQUENCY)) + pca9685 = Pvariable(PCA9685_COMPONENT_TYPE, conf[CONF_ID], rhs) + if CONF_ADDRESS in conf: + add(pca9685.set_address(HexIntLiteral(conf[CONF_ADDRESS]))) + if CONF_PHASE_BALANCER in conf: + phase_balancer = RawExpression(u'PCA9685_PhaseBalancer_{}'.format( + conf[CONF_PHASE_BALANCER])) + add(pca9685.set_phase_balancer(phase_balancer)) diff --git a/esphomeyaml/components/power_supply.py b/esphomeyaml/components/power_supply.py new file mode 100644 index 0000000000..78fe375469 --- /dev/null +++ b/esphomeyaml/components/power_supply.py @@ -0,0 +1,25 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml import pins +from esphomeyaml.const import CONF_ENABLE_TIME, CONF_ID, CONF_KEEP_ON_TIME, CONF_PIN +from esphomeyaml.helpers import App, Pvariable, add, exp_gpio_output_pin + +POWER_SUPPLY_SCHEMA = cv.REQUIRED_ID_SCHEMA.extend({ + vol.Required(CONF_PIN): pins.GPIO_OUTPUT_PIN_SCHEMA, + vol.Optional(CONF_ENABLE_TIME): cv.positive_time_period, + vol.Optional(CONF_KEEP_ON_TIME): cv.positive_time_period, +}) + +CONFIG_SCHEMA = vol.All(cv.ensure_list, [POWER_SUPPLY_SCHEMA]) + + +def to_code(config): + for conf in config: + pin = exp_gpio_output_pin(conf[CONF_PIN]) + rhs = App.make_power_supply(pin) + psu = Pvariable('PowerSupplyComponent', conf[CONF_ID], rhs) + if CONF_ENABLE_TIME in conf: + add(psu.set_enable_time(conf[CONF_ENABLE_TIME])) + if CONF_KEEP_ON_TIME in conf: + add(psu.set_keep_on_time(conf[CONF_KEEP_ON_TIME])) diff --git a/esphomeyaml/components/sensor/__init__.py b/esphomeyaml/components/sensor/__init__.py new file mode 100644 index 0000000000..87f295d898 --- /dev/null +++ b/esphomeyaml/components/sensor/__init__.py @@ -0,0 +1,100 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.const import CONF_ACCURACY_DECIMALS, CONF_ALPHA, CONF_EXPIRE_AFTER, \ + CONF_EXPONENTIAL_MOVING_AVERAGE, CONF_FILTERS, CONF_FILTER_NAN, CONF_FILTER_OUT, CONF_ICON, \ + CONF_ID, CONF_LAMBDA, CONF_MULTIPLY, CONF_NAME, CONF_OFFSET, CONF_SEND_EVERY, \ + CONF_SLIDING_WINDOW_MOVING_AVERAGE, CONF_UNIT_OF_MEASUREMENT, CONF_WINDOW_SIZE +from esphomeyaml.helpers import App, ArrayInitializer, MockObj, Pvariable, RawExpression, add, \ + setup_mqtt_component + +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ + +}) + +FILTERS_SCHEMA = vol.All(cv.ensure_list, [vol.Any( + # TODO Fix weird voluptuous error messages + vol.Schema({vol.Required(CONF_OFFSET): vol.Coerce(float)}), + vol.Schema({vol.Required(CONF_MULTIPLY): vol.Coerce(float)}), + vol.Schema({vol.Required(CONF_FILTER_OUT): vol.Coerce(float)}), + vol.Schema({vol.Required(CONF_FILTER_NAN): None}), + vol.Schema({ + vol.Required(CONF_SLIDING_WINDOW_MOVING_AVERAGE): vol.Schema({ + vol.Required(CONF_WINDOW_SIZE): cv.positive_not_null_int, + vol.Required(CONF_SEND_EVERY): cv.positive_not_null_int, + }) + }), + vol.Schema({ + vol.Required(CONF_EXPONENTIAL_MOVING_AVERAGE): vol.Schema({ + vol.Required(CONF_ALPHA): cv.positive_float, + vol.Required(CONF_SEND_EVERY): cv.positive_not_null_int, + }) + }), + vol.Schema({vol.Required(CONF_LAMBDA): cv.string_strict}), +)]) + +MQTT_SENSOR_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string_strict, + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(CONF_ACCURACY_DECIMALS): vol.Coerce(int), + vol.Optional(CONF_EXPIRE_AFTER): vol.Any(None, cv.positive_time_period), + vol.Optional(CONF_FILTERS): FILTERS_SCHEMA +}) + +MQTT_SENSOR_ID_SCHEMA = MQTT_SENSOR_SCHEMA.extend({ + cv.GenerateID('mqtt_sensor'): cv.register_variable_id, +}) + +OffsetFilter = MockObj('new sensor::OffsetFilter') +MultiplyFilter = MockObj('new sensor::MultiplyFilter') +FilterOutValueFilter = MockObj('new sensor::FilterOutValueFilter') +FilterOutNANFilter = MockObj('new sensor::FilterOutNANFilter') +SlidingWindowMovingAverageFilter = MockObj('new sensor::SlidingWindowMovingAverageFilter') +ExponentialMovingAverageFilter = MockObj('new sensor::ExponentialMovingAverageFilter') +LambdaFilter = MockObj('new sensor::LambdaFilter') + + +def setup_filter(config): + if CONF_OFFSET in config: + return OffsetFilter(config[CONF_OFFSET]) + if CONF_MULTIPLY in config: + return MultiplyFilter(config[CONF_MULTIPLY]) + if CONF_FILTER_OUT in config: + return FilterOutValueFilter(config[CONF_FILTER_OUT]) + if CONF_FILTER_NAN in config: + return FilterOutNANFilter() + if CONF_SLIDING_WINDOW_MOVING_AVERAGE in config: + conf = config[CONF_SLIDING_WINDOW_MOVING_AVERAGE] + return SlidingWindowMovingAverageFilter(conf[CONF_WINDOW_SIZE], conf[CONF_SEND_EVERY]) + if CONF_EXPONENTIAL_MOVING_AVERAGE in config: + conf = config[CONF_EXPONENTIAL_MOVING_AVERAGE] + return ExponentialMovingAverageFilter(conf[CONF_ALPHA], conf[CONF_SEND_EVERY]) + if CONF_LAMBDA in config: + s = '[](float x) -> Optional {{ return {}; }}'.format(config[CONF_LAMBDA]) + return LambdaFilter(RawExpression(s)) + raise ValueError("Filter unsupported: {}".format(config)) + + +def setup_mqtt_sensor_component(obj, config): + if CONF_UNIT_OF_MEASUREMENT in config: + add(obj.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT])) + if CONF_ICON in config: + add(obj.set_icon(config[CONF_ICON])) + if CONF_ACCURACY_DECIMALS in config: + add(obj.set_accuracy_decimals(config[CONF_ACCURACY_DECIMALS])) + if CONF_EXPIRE_AFTER in config: + if config[CONF_EXPIRE_AFTER] is None: + add(obj.disable_expire_after()) + else: + add(obj.set_expire_after(config[CONF_EXPIRE_AFTER])) + if CONF_FILTERS in config: + filters = [setup_filter(x) for x in config[CONF_FILTERS]] + add(obj.set_filters(ArrayInitializer(*filters))) + setup_mqtt_component(obj, config) + + +def make_mqtt_sensor_for(exp, config): + rhs = App.make_mqtt_sensor_for(exp, config[CONF_NAME]) + mqtt_sensor = Pvariable('sensor::MQTTSensorComponent', config[CONF_ID], rhs) + setup_mqtt_sensor_component(mqtt_sensor, config) diff --git a/esphomeyaml/components/sensor/adc.py b/esphomeyaml/components/sensor/adc.py new file mode 100644 index 0000000000..6deb531144 --- /dev/null +++ b/esphomeyaml/components/sensor/adc.py @@ -0,0 +1,35 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml import pins +from esphomeyaml.components import sensor +from esphomeyaml.const import CONF_ATTENUATION, CONF_ID, CONF_NAME, CONF_PIN, \ + CONF_UPDATE_INTERVAL +from esphomeyaml.helpers import App, RawExpression, add, variable + +ATTENUATION_MODES = { + '0db': 'ADC_0db', + '2.5db': 'ADC_2_5db', + '6db': 'ADC_6db', + '11db': 'ADC_11db', +} + +ATTENUATION_MODE_SCHEMA = vol.Any(*list(ATTENUATION_MODES.keys())) + +PLATFORM_SCHEMA = sensor.PLATFORM_SCHEMA.extend({ + cv.GenerateID('adc'): cv.register_variable_id, + vol.Required(CONF_PIN): pins.analog_pin, + vol.Optional(CONF_ATTENUATION): vol.All(cv.only_on_esp32, ATTENUATION_MODE_SCHEMA), + vol.Optional(CONF_UPDATE_INTERVAL): cv.positive_not_null_time_period, +}).extend(sensor.MQTT_SENSOR_SCHEMA.schema) + + +def to_code(config): + rhs = App.make_adc_sensor(config[CONF_PIN], config[CONF_NAME], + config.get(CONF_UPDATE_INTERVAL)) + make = variable('Application::MakeADCSensor', config[CONF_ID], rhs) + adc = make.Padc + if CONF_ATTENUATION in config: + attenuation = ATTENUATION_MODES[config[CONF_ATTENUATION]] + add(adc.set_attenuation(RawExpression(attenuation))) + sensor.setup_mqtt_sensor_component(make.Pmqtt, config) diff --git a/esphomeyaml/components/sensor/ads1115.py b/esphomeyaml/components/sensor/ads1115.py new file mode 100644 index 0000000000..55d7740a29 --- /dev/null +++ b/esphomeyaml/components/sensor/ads1115.py @@ -0,0 +1,56 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.components import sensor +from esphomeyaml.const import CONF_ADS1115_ID, CONF_GAIN, CONF_MULTIPLEXER, CONF_UPDATE_INTERVAL +from esphomeyaml.helpers import get_variable, RawExpression + +DEPENDENCIES = ['ads1115'] + +MUX = { + 'A0_A1': 'ADS1115_MUX_P0_N1', + 'A0_A3': 'ADS1115_MUX_P0_N3', + 'A1_A3': 'ADS1115_MUX_P1_N3', + 'A2_A3': 'ADS1115_MUX_P2_N3', + 'A0_GND': 'ADS1115_MUX_P0_NG', + 'A1_GND': 'ADS1115_MUX_P1_NG', + 'A2_GND': 'ADS1115_MUX_P2_NG', + 'A3_GND': 'ADS1115_MUX_P3_NG', +} + +GAIN = { + '6.144': 'ADS1115_PGA_6P144', + '4.096': 'ADS1115_PGA_6P096', + '2.048': 'ADS1115_PGA_2P048', + '1.024': 'ADS1115_PGA_1P024', + '0.512': 'ADS1115_PGA_0P512', + '0.256': 'ADS1115_PGA_0P256', +} + + +def validate_gain(value): + if isinstance(value, float): + value = u'{:0.03f}'.format(value) + elif not isinstance(value, (str, unicode)): + raise vol.Invalid('invalid gain "{}"'.format(value)) + + if value not in GAIN: + raise vol.Invalid("Invalid gain, options are {}".format(', '.join(GAIN.keys()))) + return value + + +PLATFORM_SCHEMA = sensor.PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MULTIPLEXER): vol.All(vol.Upper, vol.Any(*list(MUX.keys()))), + vol.Required(CONF_GAIN): validate_gain, + vol.Optional(CONF_ADS1115_ID): cv.variable_id, + vol.Optional(CONF_UPDATE_INTERVAL): cv.positive_not_null_time_period, +}).extend(sensor.MQTT_SENSOR_ID_SCHEMA.schema) + + +def to_code(config): + hub = get_variable(config.get(CONF_ADS1115_ID), u'sensor::ADS1115Component') + + mux = RawExpression(MUX[config[CONF_MULTIPLEXER]]) + gain = RawExpression(GAIN[config[CONF_GAIN]]) + sensor_ = hub.get_sensor(mux, gain, config.get(CONF_UPDATE_INTERVAL)) + sensor.make_mqtt_sensor_for(sensor_, config) diff --git a/esphomeyaml/components/sensor/bmp085.py b/esphomeyaml/components/sensor/bmp085.py new file mode 100644 index 0000000000..3db8e71082 --- /dev/null +++ b/esphomeyaml/components/sensor/bmp085.py @@ -0,0 +1,29 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.components import sensor +from esphomeyaml.components.sensor import MQTT_SENSOR_SCHEMA +from esphomeyaml.const import CONF_ADDRESS, CONF_ID, CONF_NAME, \ + CONF_PRESSURE, CONF_TEMPERATURE, CONF_UPDATE_INTERVAL +from esphomeyaml.helpers import App, HexIntLiteral, add, variable + +DEPENDENCIES = ['i2c'] + +PLATFORM_SCHEMA = sensor.PLATFORM_SCHEMA.extend({ + cv.GenerateID('bmp085_sensor'): cv.register_variable_id, + vol.Required(CONF_TEMPERATURE): MQTT_SENSOR_SCHEMA, + vol.Required(CONF_PRESSURE): MQTT_SENSOR_SCHEMA, + vol.Optional(CONF_ADDRESS): cv.i2c_address, + vol.Optional(CONF_UPDATE_INTERVAL): cv.positive_not_null_time_period, +}) + + +def to_code(config): + rhs = App.make_bmp085_sensor(config[CONF_TEMPERATURE][CONF_NAME], + config[CONF_PRESSURE][CONF_NAME], + config.get(CONF_UPDATE_INTERVAL)) + bmp = variable('Application::MakeBMP085Component', config[CONF_ID], rhs) + if CONF_ADDRESS in config: + add(bmp.Pbmp.set_address(HexIntLiteral(config[CONF_ADDRESS]))) + sensor.setup_mqtt_sensor_component(bmp.Pmqtt_temperature, config[CONF_TEMPERATURE]) + sensor.setup_mqtt_sensor_component(bmp.Pmqtt_pressure, config[CONF_PRESSURE]) diff --git a/esphomeyaml/components/sensor/dallas.py b/esphomeyaml/components/sensor/dallas.py new file mode 100644 index 0000000000..ee560967b3 --- /dev/null +++ b/esphomeyaml/components/sensor/dallas.py @@ -0,0 +1,31 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.components import sensor +from esphomeyaml.components.dallas import DALLAS_COMPONENT_CLASS +from esphomeyaml.const import CONF_ADDRESS, CONF_DALLAS_ID, CONF_INDEX, CONF_RESOLUTION, \ + CONF_UPDATE_INTERVAL +from esphomeyaml.helpers import HexIntLiteral, get_variable + +PLATFORM_SCHEMA = sensor.PLATFORM_SCHEMA.extend({ + vol.Exclusive(CONF_ADDRESS, 'dallas'): cv.hex_int, + vol.Exclusive(CONF_INDEX, 'dallas'): cv.positive_int, + vol.Optional(CONF_DALLAS_ID): cv.variable_id, + vol.Optional(CONF_RESOLUTION): vol.All(vol.Coerce(int), vol.Range(min=8, max=12)), +}).extend(sensor.MQTT_SENSOR_ID_SCHEMA.schema) + + +def to_code(config): + hub = get_variable(config.get(CONF_DALLAS_ID), DALLAS_COMPONENT_CLASS) + update_interval = config.get(CONF_UPDATE_INTERVAL) + if CONF_RESOLUTION in config and update_interval is None: + update_interval = 10000 + + if CONF_ADDRESS in config: + address = HexIntLiteral(config[CONF_ADDRESS]) + sensor_ = hub.Pget_sensor_by_address(address, update_interval, + config.get(CONF_RESOLUTION)) + else: + sensor_ = hub.Pget_sensor_by_index(config[CONF_INDEX], update_interval, + config.get(CONF_RESOLUTION)) + sensor.make_mqtt_sensor_for(sensor_, config) diff --git a/esphomeyaml/components/sensor/dht.py b/esphomeyaml/components/sensor/dht.py new file mode 100644 index 0000000000..810693ba8c --- /dev/null +++ b/esphomeyaml/components/sensor/dht.py @@ -0,0 +1,31 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml import pins +from esphomeyaml.components import sensor +from esphomeyaml.components.sensor import MQTT_SENSOR_SCHEMA +from esphomeyaml.const import CONF_HUMIDITY, CONF_ID, CONF_MODEL, CONF_NAME, CONF_PIN, \ + CONF_TEMPERATURE, CONF_UPDATE_INTERVAL +from esphomeyaml.helpers import App, RawExpression, add, variable + +DHT_MODELS = ['AUTO_DETECT', 'DHT11', 'DHT22', 'AM2302', 'RHT03'] + +PLATFORM_SCHEMA = sensor.PLATFORM_SCHEMA.extend({ + cv.GenerateID('dht_sensor'): cv.register_variable_id, + vol.Required(CONF_PIN): pins.input_output_pin, + vol.Required(CONF_TEMPERATURE): MQTT_SENSOR_SCHEMA, + vol.Required(CONF_HUMIDITY): MQTT_SENSOR_SCHEMA, + vol.Optional(CONF_MODEL): vol.All(vol.Upper, vol.Any(*DHT_MODELS)), + vol.Optional(CONF_UPDATE_INTERVAL): cv.positive_not_null_time_period, +}) + + +def to_code(config): + rhs = App.make_dht_sensor(config[CONF_PIN], config[CONF_TEMPERATURE][CONF_NAME], + config[CONF_HUMIDITY][CONF_NAME], config.get(CONF_UPDATE_INTERVAL)) + dht = variable('Application::MakeDHTComponent', config[CONF_ID], rhs) + if CONF_MODEL in config: + model = RawExpression('DHT::{}'.format(config[CONF_MODEL])) + add(dht.Pdht.set_dht_model(model)) + sensor.setup_mqtt_sensor_component(dht.Pmqtt_temperature, config[CONF_TEMPERATURE]) + sensor.setup_mqtt_sensor_component(dht.Pmqtt_humidity, config[CONF_HUMIDITY]) diff --git a/esphomeyaml/components/sensor/hdc1080.py b/esphomeyaml/components/sensor/hdc1080.py new file mode 100644 index 0000000000..89d4e27685 --- /dev/null +++ b/esphomeyaml/components/sensor/hdc1080.py @@ -0,0 +1,26 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.components import sensor +from esphomeyaml.components.sensor import MQTT_SENSOR_SCHEMA +from esphomeyaml.const import CONF_HUMIDITY, CONF_ID, CONF_NAME, CONF_TEMPERATURE, \ + CONF_UPDATE_INTERVAL +from esphomeyaml.helpers import App, variable + +DEPENDENCIES = ['i2c'] + +PLATFORM_SCHEMA = sensor.PLATFORM_SCHEMA.extend({ + cv.GenerateID('dht_sensor'): cv.register_variable_id, + vol.Required(CONF_TEMPERATURE): MQTT_SENSOR_SCHEMA, + vol.Required(CONF_HUMIDITY): MQTT_SENSOR_SCHEMA, + vol.Optional(CONF_UPDATE_INTERVAL): cv.positive_not_null_time_period, +}) + + +def to_code(config): + rhs = App.make_hdc1080_sensor(config[CONF_TEMPERATURE][CONF_NAME], + config[CONF_HUMIDITY][CONF_NAME], + config.get(CONF_UPDATE_INTERVAL)) + hdc1080 = variable('Application::MakeHDC1080Component', config[CONF_ID], rhs) + sensor.setup_mqtt_sensor_component(hdc1080.Pmqtt_temperature, config[CONF_TEMPERATURE]) + sensor.setup_mqtt_sensor_component(hdc1080.Pmqtt_humidity, config[CONF_HUMIDITY]) diff --git a/esphomeyaml/components/sensor/htu21d.py b/esphomeyaml/components/sensor/htu21d.py new file mode 100644 index 0000000000..876a33396c --- /dev/null +++ b/esphomeyaml/components/sensor/htu21d.py @@ -0,0 +1,26 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.components import sensor +from esphomeyaml.components.sensor import MQTT_SENSOR_SCHEMA +from esphomeyaml.const import CONF_HUMIDITY, CONF_ID, CONF_NAME, CONF_TEMPERATURE, \ + CONF_UPDATE_INTERVAL +from esphomeyaml.helpers import App, variable + +DEPENDENCIES = ['i2c'] + +PLATFORM_SCHEMA = sensor.PLATFORM_SCHEMA.extend({ + cv.GenerateID('htu21d'): cv.register_variable_id, + vol.Required(CONF_TEMPERATURE): MQTT_SENSOR_SCHEMA, + vol.Required(CONF_HUMIDITY): MQTT_SENSOR_SCHEMA, + vol.Optional(CONF_UPDATE_INTERVAL): cv.positive_not_null_time_period, +}) + + +def to_code(config): + rhs = App.make_htu21d_sensor(config[CONF_TEMPERATURE][CONF_NAME], + config[CONF_HUMIDITY][CONF_NAME], + config.get(CONF_UPDATE_INTERVAL)) + htu21d = variable('Application::MakeHTU21DComponent', config[CONF_ID], rhs) + sensor.setup_mqtt_sensor_component(htu21d.Pmqtt_temperature, config[CONF_TEMPERATURE]) + sensor.setup_mqtt_sensor_component(htu21d.Pmqtt_humidity, config[CONF_HUMIDITY]) diff --git a/esphomeyaml/components/sensor/pulse_counter.py b/esphomeyaml/components/sensor/pulse_counter.py new file mode 100644 index 0000000000..c9e03e1a16 --- /dev/null +++ b/esphomeyaml/components/sensor/pulse_counter.py @@ -0,0 +1,58 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml import pins +from esphomeyaml.components import sensor +from esphomeyaml.const import CONF_COUNT_MODE, CONF_FALLING_EDGE, CONF_ID, CONF_INTERNAL_FILTER, \ + CONF_NAME, CONF_PIN, CONF_PULL_MODE, CONF_RISING_EDGE, CONF_UPDATE_INTERVAL, \ + ESP_PLATFORM_ESP32 +from esphomeyaml.helpers import App, RawExpression, add, variable + +ESP_PLATFORMS = [ESP_PLATFORM_ESP32] + +GPIO_PULL_MODES = { + 'PULLUP': 'GPIO_PULLUP_ONLY', + 'PULLDOWN': 'GPIO_PULLDOWN_ONLY', + 'PULLUP_PULLDOWN': 'GPIO_PULLUP_PULLDOWN', + 'FLOATING': 'GPIO_FLOATING', +} + +GPIO_PULL_MODE_SCHEMA = vol.All(vol.Upper, vol.Any(*list(GPIO_PULL_MODES.keys()))) + +COUNT_MODES = { + 'DISABLE': 'PCNT_COUNT_DIS', + 'INCREMENT': 'PCNT_COUNT_INC', + 'DECREMENT': 'PCNT_COUNT_DEC', +} + +COUNT_MODE_SCHEMA = vol.All(vol.Upper, vol.Any(*list(COUNT_MODES.keys()))) + +PLATFORM_SCHEMA = sensor.PLATFORM_SCHEMA.extend({ + cv.GenerateID('pulse_counter'): cv.register_variable_id, + vol.Required(CONF_PIN): pins.input_pin, + vol.Optional(CONF_PULL_MODE): GPIO_PULL_MODE_SCHEMA, + vol.Optional(CONF_COUNT_MODE): vol.Schema({ + vol.Required(CONF_RISING_EDGE): COUNT_MODE_SCHEMA, + vol.Required(CONF_FALLING_EDGE): COUNT_MODE_SCHEMA, + }), + vol.Optional(CONF_INTERNAL_FILTER): vol.All(vol.Coerce(int), vol.Range(min=0, max=1023)), + vol.Optional(CONF_UPDATE_INTERVAL): cv.positive_not_null_time_period, +}).extend(sensor.MQTT_SENSOR_SCHEMA.schema) + + +def to_code(config): + rhs = App.make_pulse_counter_sensor(config[CONF_PIN], config[CONF_NAME], + config.get(CONF_UPDATE_INTERVAL)) + make = variable('Application::MakePulseCounter', config[CONF_ID], rhs) + pcnt = make.Ppcnt + if CONF_PULL_MODE in config: + pull_mode = GPIO_PULL_MODES[config[CONF_PULL_MODE]] + add(pcnt.set_pull_mode(RawExpression(pull_mode))) + if CONF_COUNT_MODE in config: + count_mode = config[CONF_COUNT_MODE] + rising_edge = COUNT_MODES[count_mode[CONF_RISING_EDGE]] + falling_edge = COUNT_MODES[count_mode[CONF_FALLING_EDGE]] + add(pcnt.set_edge_mode(RawExpression(rising_edge), RawExpression(falling_edge))) + if CONF_INTERNAL_FILTER in config: + add(pcnt.set_filter(config[CONF_INTERNAL_FILTER])) + sensor.setup_mqtt_sensor_component(make.Pmqtt, config) diff --git a/esphomeyaml/components/sensor/ultrasonic.py b/esphomeyaml/components/sensor/ultrasonic.py new file mode 100644 index 0000000000..05d0e44bfa --- /dev/null +++ b/esphomeyaml/components/sensor/ultrasonic.py @@ -0,0 +1,32 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml import pins +from esphomeyaml.components import sensor +from esphomeyaml.const import CONF_ECHO_PIN, CONF_ID, CONF_NAME, \ + CONF_TIMEOUT_METER, CONF_TIMEOUT_TIME, CONF_TRIGGER_PIN, CONF_UPDATE_INTERVAL +from esphomeyaml.helpers import App, add, exp_gpio_input_pin, exp_gpio_output_pin, \ + variable + +PLATFORM_SCHEMA = sensor.PLATFORM_SCHEMA.extend({ + cv.GenerateID('ultrasonic'): cv.register_variable_id, + vol.Required(CONF_TRIGGER_PIN): pins.GPIO_OUTPUT_PIN_SCHEMA, + vol.Required(CONF_ECHO_PIN): pins.GPIO_INPUT_PIN_SCHEMA, + vol.Exclusive(CONF_TIMEOUT_METER, 'timeout'): cv.positive_float, + vol.Exclusive(CONF_TIMEOUT_TIME, 'timeout'): cv.positive_int, + vol.Optional(CONF_UPDATE_INTERVAL): cv.positive_not_null_time_period, +}).extend(sensor.MQTT_SENSOR_SCHEMA.schema) + + +def to_code(config): + trigger = exp_gpio_output_pin(config[CONF_TRIGGER_PIN]) + echo = exp_gpio_input_pin(config[CONF_ECHO_PIN]) + rhs = App.make_ultrasonic_sensor(trigger, echo, config[CONF_NAME], + config.get(CONF_UPDATE_INTERVAL)) + make = variable('Application::MakeUltrasonicSensor', config[CONF_ID], rhs) + ultrasonic = make.Pultrasonic + if CONF_TIMEOUT_TIME in config: + add(ultrasonic.set_timeout_us(config[CONF_TIMEOUT_TIME])) + elif CONF_TIMEOUT_METER in config: + add(ultrasonic.set_timeout_m(config[CONF_TIMEOUT_METER])) + sensor.setup_mqtt_sensor_component(make.Pmqtt, config) diff --git a/esphomeyaml/components/switch/__init__.py b/esphomeyaml/components/switch/__init__.py new file mode 100644 index 0000000000..6afeb203fb --- /dev/null +++ b/esphomeyaml/components/switch/__init__.py @@ -0,0 +1,25 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.const import CONF_ICON, CONF_ID, CONF_NAME +from esphomeyaml.helpers import App, Pvariable, add, setup_mqtt_component + +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ + +}) + +MQTT_SWITCH_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend({ + vol.Optional(CONF_ICON): cv.icon, +}) + + +def setup_mqtt_switch(obj, config): + if CONF_ICON in config: + add(obj.set_icon(config[CONF_ICON])) + setup_mqtt_component(obj, config) + + +def make_mqtt_switch_for(exp, config): + rhs = App.make_mqtt_switch_for(exp, config[CONF_NAME]) + mqtt_switch = Pvariable('switch_::MQTTSwitchComponent', config[CONF_ID], rhs) + setup_mqtt_switch(mqtt_switch, config) diff --git a/esphomeyaml/components/switch/gpio.py b/esphomeyaml/components/switch/gpio.py new file mode 100644 index 0000000000..852c0753af --- /dev/null +++ b/esphomeyaml/components/switch/gpio.py @@ -0,0 +1,18 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml import pins +from esphomeyaml.components import switch +from esphomeyaml.const import CONF_ID, CONF_NAME, CONF_PIN +from esphomeyaml.helpers import App, exp_gpio_output_pin, variable + +PLATFORM_SCHEMA = switch.PLATFORM_SCHEMA.extend({ + cv.GenerateID('gpio_switch'): cv.register_variable_id, + vol.Required(CONF_PIN): pins.GPIO_OUTPUT_PIN_SCHEMA, +}).extend(switch.MQTT_SWITCH_SCHEMA.schema) + + +def to_code(config): + rhs = App.make_gpio_switch(exp_gpio_output_pin(config[CONF_PIN]), config[CONF_NAME]) + gpio = variable('Application::GPIOSwitchStruct', config[CONF_ID], rhs) + switch.setup_mqtt_switch(gpio.Pmqtt, config) diff --git a/esphomeyaml/components/switch/ir_transmitter.py b/esphomeyaml/components/switch/ir_transmitter.py new file mode 100644 index 0000000000..3aa9f574f0 --- /dev/null +++ b/esphomeyaml/components/switch/ir_transmitter.py @@ -0,0 +1,87 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.components import switch +from esphomeyaml.components.ir_transmitter import IR_TRANSMITTER_COMPONENT_CLASS +from esphomeyaml.const import CONF_ADDRESS, CONF_COMMAND, CONF_DATA, CONF_IR_TRANSMITTER_ID, \ + CONF_LG, CONF_NBITS, CONF_NEC, CONF_PANASONIC, CONF_REPEAT, CONF_SONY, CONF_TIMES, \ + CONF_WAIT_TIME_US, CONF_RAW, CONF_CARRIER_FREQUENCY +from esphomeyaml.core import ESPHomeYAMLError +from esphomeyaml.helpers import HexIntLiteral, MockObj, get_variable, ArrayInitializer + +PLATFORM_SCHEMA = switch.PLATFORM_SCHEMA.extend({ + cv.GenerateID('ir_transmitter'): cv.register_variable_id, + vol.Exclusive(CONF_NEC, 'code'): vol.Schema({ + vol.Required(CONF_ADDRESS): cv.hex_uint16_t, + vol.Required(CONF_COMMAND): cv.hex_uint16_t, + }), + vol.Exclusive(CONF_LG, 'code'): vol.Schema({ + vol.Required(CONF_DATA): cv.hex_uint32_t, + vol.Optional(CONF_NBITS, default=28): vol.All(vol.Coerce(int), vol.Range(min=0, max=32)), + }), + vol.Exclusive(CONF_SONY, 'code'): vol.Schema({ + vol.Required(CONF_DATA): cv.hex_uint32_t, + vol.Optional(CONF_NBITS, default=12): vol.All(vol.Coerce(int), vol.Range(min=0, max=32)), + }), + vol.Exclusive(CONF_PANASONIC, 'code'): vol.Schema({ + vol.Required(CONF_ADDRESS): cv.hex_uint16_t, + vol.Required(CONF_COMMAND): cv.hex_uint32_t, + }), + vol.Exclusive(CONF_RAW, 'code'): vol.Schema({ + vol.Required(CONF_CARRIER_FREQUENCY): vol.All(cv.frequency, vol.Coerce(int)), + vol.Required(CONF_DATA): [vol.Coerce(int)], + }), + vol.Optional(CONF_REPEAT): vol.Any(cv.positive_not_null_int, vol.Schema({ + vol.Required(CONF_TIMES): cv.positive_not_null_int, + vol.Required(CONF_WAIT_TIME_US): cv.uint32_t, + })), + vol.Optional(CONF_IR_TRANSMITTER_ID): cv.variable_id, +}).extend(switch.MQTT_SWITCH_SCHEMA.schema) + +SendData = MockObj('switch_::ir::SendData', '::') + + +def safe_hex(value): + if value is None: + return None + return HexIntLiteral(value) + + +def exp_send_data(config): + if CONF_NEC in config: + conf = config[CONF_NEC] + base = SendData.from_nec(safe_hex(conf[CONF_ADDRESS]), + safe_hex(conf[CONF_COMMAND])) + elif CONF_LG in config: + conf = config[CONF_LG] + base = SendData.from_lg(safe_hex(conf[CONF_DATA]), conf.get(CONF_NBITS)) + elif CONF_SONY in config: + conf = config[CONF_SONY] + base = SendData.from_sony(safe_hex(conf[CONF_DATA]), conf.get(CONF_NBITS)) + elif CONF_PANASONIC in config: + conf = config[CONF_PANASONIC] + base = SendData.from_panasonic(safe_hex(conf[CONF_ADDRESS]), + safe_hex(conf[CONF_COMMAND])) + elif CONF_RAW in config: + conf = config[CONF_RAW] + data = ArrayInitializer(*conf[CONF_DATA]) + base = SendData.from_raw(data, conf[CONF_CARRIER_FREQUENCY]) + else: + raise ESPHomeYAMLError(u"Unsupported IR mode {}".format(config)) + + if CONF_REPEAT in config: + if isinstance(config[CONF_REPEAT], int): + times = config[CONF_REPEAT] + wait_us = None + else: + times = config[CONF_REPEAT][CONF_TIMES] + wait_us = config[CONF_REPEAT][CONF_WAIT_TIME_US] + base = MockObj(unicode(base), u'.') + base = base.repeat(times, wait_us) + return base + + +def to_code(config): + ir = get_variable(config.get(CONF_IR_TRANSMITTER_ID), IR_TRANSMITTER_COMPONENT_CLASS) + send_data = exp_send_data(config) + switch.make_mqtt_switch_for(ir.create_transmitter(send_data), config) diff --git a/esphomeyaml/components/switch/restart.py b/esphomeyaml/components/switch/restart.py new file mode 100644 index 0000000000..ff3a8e6c00 --- /dev/null +++ b/esphomeyaml/components/switch/restart.py @@ -0,0 +1,14 @@ +import esphomeyaml.config_validation as cv +from esphomeyaml.components import switch +from esphomeyaml.const import CONF_ID, CONF_NAME +from esphomeyaml.helpers import App, Pvariable + +PLATFORM_SCHEMA = switch.PLATFORM_SCHEMA.extend({ + cv.GenerateID('restart_switch'): cv.register_variable_id, +}).extend(switch.MQTT_SWITCH_SCHEMA.schema) + + +def to_code(config): + rhs = App.make_restart_switch(config[CONF_NAME]) + mqtt = Pvariable('switch_::MQTTSwitchComponent', config[CONF_ID], rhs) + switch.setup_mqtt_switch(mqtt, config) diff --git a/esphomeyaml/components/wifi.py b/esphomeyaml/components/wifi.py new file mode 100644 index 0000000000..0d56646064 --- /dev/null +++ b/esphomeyaml/components/wifi.py @@ -0,0 +1,46 @@ +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 +from esphomeyaml.helpers import App, MockObj, Pvariable, StructInitializer, add + +CONFIG_SCHEMA = cv.ID_SCHEMA.extend({ + 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_HOSTNAME): cv.hostname, +}) + +IPAddress = MockObj('IPAddress') + + +def safe_ip(ip): + if ip is None: + return None + return IPAddress(*ip.args) + + +def to_code(config): + rhs = App.init_wifi(config[CONF_SSID], config.get(CONF_PASSWORD)) + 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 CONF_HOSTNAME in config: + add(wifi.set_hostname(config[CONF_HOSTNAME])) diff --git a/esphomeyaml/config.py b/esphomeyaml/config.py new file mode 100644 index 0000000000..0550152473 --- /dev/null +++ b/esphomeyaml/config.py @@ -0,0 +1,280 @@ +from __future__ import print_function + +import importlib +import logging +from collections import OrderedDict + +import voluptuous as vol +from voluptuous.humanize import humanize_error + +import esphomeyaml.config_validation as cv +from esphomeyaml import helpers, 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 +from esphomeyaml.core import ESPHomeYAMLError +from esphomeyaml.helpers import App, add, add_task, color + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_LIBRARY_URI = u'esphomelib' + +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_BOARD): cv.string, + vol.Optional(CONF_LIBRARY_URI, default=DEFAULT_LIBRARY_URI): cv.string, + vol.Optional(CONF_SIMPLIFY, default=True): cv.boolean, +}) + +REQUIRED_COMPONENTS = [ + CONF_ESPHOMEYAML, CONF_WIFI, CONF_MQTT +] + +_COMPONENT_CACHE = {} +_ALL_COMPONENTS = [] + + +def core_to_code(config): + add(App.set_name(config[CONF_NAME])) + + +def get_component(domain): + if domain in _COMPONENT_CACHE: + return _COMPONENT_CACHE[domain] + + path = 'esphomeyaml.components.{}'.format(domain) + try: + module = importlib.import_module(path) + except ImportError as err: + _LOGGER.debug(err) + pass + else: + _COMPONENT_CACHE[domain] = module + return module + + _LOGGER.error("Unable to find component %s", domain) + return None + + +def get_platform(domain, platform): + return get_component("{}.{}".format(domain, platform)) + + +def is_platform_component(component): + return hasattr(component, 'PLATFORM_SCHEMA') + + +def validate_schema(config, schema): + return schema(config) + + +class Config(OrderedDict): + def __init__(self): + super(Config, self).__init__() + self.errors = [] + + def add_error(self, message, domain=None, config=None): + if not isinstance(message, unicode): + message = unicode(message) + self.errors.append((message, domain, config)) + + +def validate_config(config): + global _ALL_COMPONENTS + + for req in REQUIRED_COMPONENTS: + if req not in config: + raise ESPHomeYAMLError("Component %s is required for esphomeyaml.", req) + + _ALL_COMPONENTS = list(config.keys()) + + result = Config() + + def _comp_error(ex, domain, config): + result.add_error(_format_config_error(ex, domain, config), domain, config) + + try: + result[CONF_ESPHOMEYAML] = validate_schema(config[CONF_ESPHOMEYAML], CORE_SCHEMA) + except vol.Invalid as ex: + _comp_error(ex, CONF_ESPHOMEYAML, config) + + for domain, conf in config.iteritems(): + if domain == CONF_ESPHOMEYAML: + continue + if conf is None: + conf = {} + component = get_component(domain) + if component is None: + result.add_error(u"Component not found: {}".format(domain)) + continue + + esp_platforms = getattr(component, 'ESP_PLATFORMS', ESP_PLATFORMS) + if cv.ESP_PLATFORM not in esp_platforms: + result.add_error(u"Component {} doesn't support {}.".format(domain, cv.ESP_PLATFORM)) + continue + + success = True + dependencies = getattr(component, 'DEPENDENCIES', []) + for dependency in dependencies: + if dependency not in _ALL_COMPONENTS: + result.add_error(u"Component {} requires {}".format(domain, dependency)) + success = False + if not success: + continue + + if hasattr(component, 'CONFIG_SCHEMA'): + try: + validated = component.CONFIG_SCHEMA(conf) + result[domain] = validated + except vol.Invalid as ex: + _comp_error(ex, domain, config) + continue + + if not hasattr(component, 'PLATFORM_SCHEMA'): + continue + + platforms = [] + for i, p_config in enumerate(conf): + if not isinstance(p_config, dict): + result.add_error(u"Platform schemas mus have 'platform:' key") + continue + p_name = p_config.get(u'platform') + if p_name is None: + result.add_error(u"No platform specified for {}".format(domain)) + continue + platform = get_platform(domain, p_name) + if platform is None: + result.add_error(u"Platform not found: {}.{}") + continue + + if hasattr(platform, u'PLATFORM_SCHEMA'): + try: + p_validated = platform.PLATFORM_SCHEMA(p_config) + except vol.Invalid as ex: + _comp_error(ex, u'{}.{}'.format(domain, p_name), p_config) + continue + platforms.append(p_validated) + result[domain] = platforms + return result + + +REQUIRED = ['esphomeyaml', 'wifi', 'mqtt'] + + +def _format_config_error(ex, domain, config): + message = u"Invalid config for [{}]: ".format(domain) + if u'extra keys not allowed' in ex.error_message: + message += u'[{}] is an invalid option for [{}]. Check: {}->{}.' \ + .format(ex.path[-1], domain, domain, + u'->'.join(str(m) for m in ex.path)) + else: + message += u'{}.'.format(humanize_error(config, ex)) + + domain_config = config.get(domain, config) + message += u" (See {}, line {}). ".format( + getattr(domain_config, '__config_file__', '?'), + getattr(domain_config, '__line__', '?')) + + return message + + +def load_config(path): + try: + config = yaml_util.load_yaml(path) + except OSError: + raise ESPHomeYAMLError(u"Could not read configuration file at {}".format(path)) + + esp_platform = unicode(config.get(CONF_ESPHOMEYAML, {}).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)) + cv.ESP_PLATFORM = esp_platform + cv.BOARD = unicode(config.get(CONF_ESPHOMEYAML, {}).get(CONF_BOARD, u"")) + helpers.SIMPLIFY = cv.boolean(config.get(CONF_SIMPLIFY, True)) + + try: + result = validate_config(config) + except Exception as e: + print(u"Unexpected exception while reading configuration:") + raise + + return result + + +def add_platform_task(domain, config): + platform_ = config[CONF_PLATFORM] + platform = get_platform(domain, platform_) + if not hasattr(platform, 'to_code'): + raise ESPHomeYAMLError(u"Platform '{}.{}' doesn't have to_code.".format(domain, platform_)) + add_task(platform.to_code, config) + + +def add_component_task(domain, config): + if domain == CONF_ESPHOMEYAML: + add_task(core_to_code, config) + return + component = get_component(domain) + if is_platform_component(component): + for conf in config: + add_platform_task(domain, conf) + else: + if not hasattr(component, 'to_code'): + raise ESPHomeYAMLError(u"Component '{}' doesn't have to_code.".format(domain)) + add_task(component.to_code, config) + + +def line_info(obj, **kwargs): + """Display line config source.""" + if hasattr(obj, '__config_file__'): + return color('cyan', "[source {}:{}]" + .format(obj.__config_file__, obj.__line__ or '?'), + **kwargs) + return '?' + + +def dump_dict(layer, indent_count=3, listi=False, **kwargs): + def sort_dict_key(val): + """Return the dict key for sorting.""" + key = str.lower(val[0]) + return '0' if key == 'platform' else key + + indent_str = indent_count * ' ' + if listi or isinstance(layer, list): + indent_str = indent_str[:-1] + '-' + if isinstance(layer, dict): + for key, value in sorted(layer.items(), key=sort_dict_key): + if isinstance(value, (dict, list)): + print(indent_str, key + ':', line_info(value, **kwargs)) + dump_dict(value, indent_count + 2) + else: + print(indent_str, key + ':', value) + indent_str = indent_count * ' ' + if isinstance(layer, (list, tuple)): + for i in layer: + if isinstance(i, dict): + dump_dict(i, indent_count + 2, True) + else: + print(' ', indent_str, i) + + +def read_config(path): + _LOGGER.debug("Reading configuration...") + res = load_config(path) + excepts = {} + for err in res.errors: + domain = err[1] or u"General Error" + excepts.setdefault(domain, []).append(err[0]) + if err[2] is not None: + excepts[domain].append(err[2]) + + if excepts: + print(color('bold_white', u"Failed config")) + for domain, config in excepts.iteritems(): + print(' ', color('bold_red', domain + ':'), color('red', '', reset='red')) + dump_dict(config, reset='red') + print(color('reset')) + return None + return dict(**res) diff --git a/esphomeyaml/config_validation.py b/esphomeyaml/config_validation.py new file mode 100644 index 0000000000..5ea9bd889f --- /dev/null +++ b/esphomeyaml/config_validation.py @@ -0,0 +1,372 @@ +# coding=utf-8 +"""Helpers for config validation using voluptuous.""" +from __future__ import print_function + +import logging +from datetime import timedelta + +import voluptuous as vol + +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, \ + ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266 +from esphomeyaml.core import HexInt, IPAddress +from esphomeyaml.helpers import ensure_unique_string + +_LOGGER = logging.getLogger(__name__) + +port = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)) +positive_float = vol.All(vol.Coerce(float), vol.Range(min=0)) +zero_to_one_float = vol.All(vol.Coerce(float), vol.Range(min=0, 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)) + +ESP_PLATFORM = '' +BOARD = '' + +ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_' + +RESERVED_IDS = [ + # C++ keywords http://en.cppreference.com/w/cpp/keyword + 'alignas', 'alignof', 'and', 'and_eq', 'asm', 'auto', 'bitand', 'bitor', 'bool', 'break', + 'case', 'catch', 'char', 'char16_t', 'char32_t', 'class', 'compl', 'concept', 'const', + 'constexpr', 'const_cast', 'continue', 'decltype', 'default', 'delete', 'do', 'double', + 'dynamic_cast', 'else', 'enum', 'explicit', 'export', 'export', 'extern', 'false', 'float', + 'for', 'friend', 'goto', 'if', 'inline', 'int', 'long', 'mutable', 'namespace', 'new', + 'noexcept', 'not', 'not_eq', 'nullptr', 'operator', 'or', 'or_eq', 'private', 'protected', + 'public', 'register', 'reinterpret_cast', 'requires', 'return', 'short', 'signed', 'sizeof', + 'static', 'static_assert', 'static_cast', 'struct', 'switch', 'template', 'this', + 'thread_local', 'throw', 'true', 'try', 'typedef', 'typeid', 'typename', 'union', 'unsigned', + 'using', 'virtual', 'void', 'volatile', 'wchar_t', 'while', 'xor', 'xor_eq', + + 'App', 'pinMode', 'delay', 'delayMicroseconds', 'digitalRead', 'digitalWrite', 'INPUT', + 'OUTPUT', +] + + +def alphanumeric(value): + if value is None: + raise vol.Invalid("string value is None") + value = unicode(value) + if not value.isalnum(): + raise vol.Invalid("string value is not alphanumeric") + return value + + +def valid_name(value): + value = string_strict(value) + if not all(c in ALLOWED_NAME_CHARS for c in value): + raise vol.Invalid(u"Valid characters for name are %s", ALLOWED_NAME_CHARS) + return value + + +def string(value): + if isinstance(value, (dict, list)): + raise vol.Invalid("string value cannot be dictionary or list.") + if value is not None: + return unicode(value) + raise vol.Invalid("string value is None") + + +def string_strict(value): + """Strictly only allow strings.""" + if isinstance(value, str) or isinstance(value, unicode): + return value + raise vol.Invalid("Must be string, did you forget putting quotes " + "around the value?") + + +def icon(value): + """Validate icon.""" + value = string_strict(value) + if value.startswith('mdi:'): + return value + raise vol.Invalid('Icons should start with prefix "mdi:"') + + +def boolean(value): + """Validate and coerce a boolean value.""" + if isinstance(value, str): + value = value.lower() + if value in ('1', 'true', 'yes', 'on', 'enable'): + return True + if value in ('0', 'false', 'no', 'off', 'disable'): + return False + raise vol.Invalid('invalid boolean value {}'.format(value)) + return bool(value) + + +def ensure_list(value): + """Wrap value in list if it is not one.""" + if value is None: + return [] + if isinstance(value, list): + return value + return [value] + + +def ensure_dict(value): + if value is None: + return {} + if not isinstance(value, dict): + raise vol.Invalid("Expected a dictionary") + return value + + +def hex_int_(value): + if isinstance(value, (int, long)): + return HexInt(value) + value = string_strict(value).lower() + if value.startswith('0x'): + return HexInt(int(value, 16)) + return HexInt(int(value)) + + +def int_(value): + if isinstance(value, (int, long)): + return value + value = string_strict(value).lower() + if value.startswith('0x'): + return int(value, 16) + return int(value) + + +hex_int = vol.Coerce(hex_int_) +match_cpp_var_ = vol.Match(r'^[a-zA-Z_][a-zA-Z0-9_]+$', msg=u"Must be a valid C++ variable name") + + +def variable_id(value): + value = match_cpp_var_(value) + if value in RESERVED_IDS: + raise vol.Invalid(u"ID {} is reserved internally and cannot be used".format(value)) + return value + + +def only_on(platforms): + if not isinstance(platforms, list): + platforms = [platforms] + + def validator_(obj): + print(obj) + if ESP_PLATFORM not in platforms: + raise vol.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) + + +# Adapted from: +# https://github.com/alecthomas/voluptuous/issues/115#issuecomment-144464666 +def has_at_least_one_key(*keys): + """Validate that at least one key exists.""" + + def validate(obj): + """Test keys exist in dict.""" + if not isinstance(obj, dict): + raise vol.Invalid('expected dictionary') + + for k in obj.keys(): + if k in keys: + return obj + raise vol.Invalid('must contain one of {}.'.format(', '.join(keys))) + + return validate + + +TIME_PERIOD_ERROR = "Time period {} should be format 5ms, 5s, 5min, 5h" + +time_period_dict = vol.All( + dict, vol.Schema({ + 'days': vol.Coerce(int), + 'hours': vol.Coerce(int), + 'minutes': vol.Coerce(int), + 'seconds': vol.Coerce(int), + 'milliseconds': vol.Coerce(int), + }), + has_at_least_one_key('days', 'hours', 'minutes', + 'seconds', 'milliseconds'), + lambda value: timedelta(**value)) + + +def time_period_str(value): + """Validate and transform time offset.""" + if isinstance(value, int): + raise vol.Invalid("Make sure you wrap time values in quotes") + elif not isinstance(value, (str, unicode)): + raise vol.Invalid(TIME_PERIOD_ERROR.format(value)) + + value = unicode(value) + if value.endswith(u'ms'): + return vol.Coerce(int)(value[:-2]) + elif value.endswith(u's'): + return vol.Coerce(float)(value[:-1]) * 1000 + elif value.endswith(u'min'): + return vol.Coerce(float)(value[:-3]) * 1000 * 60 + elif value.endswith(u'h'): + return vol.Coerce(float)(value[:-1]) * 1000 * 60 * 60 + raise vol.Invalid(TIME_PERIOD_ERROR.format(value)) + + +def time_period_milliseconds(value): + try: + return timedelta(milliseconds=int(value)) + except (ValueError, TypeError): + raise vol.Invalid('Expected milliseconds, got {}'.format(value)) + + +def time_period_to_milliseconds(value): + if isinstance(value, (int, long)): + return value + if isinstance(value, float): + return int(value) + return value / timedelta(milliseconds=1) + + +time_period = vol.All(vol.Any(time_period_str, timedelta, time_period_dict, + time_period_milliseconds), time_period_to_milliseconds) +positive_time_period = vol.All(time_period, vol.Range(min=0)) +positive_not_null_time_period = vol.All(time_period, vol.Range(min=0, min_included=False)) + + +METRIC_SUFFIXES = { + 'E': 1e18, 'P': 1e15, 'T': 1e12, 'G': 1e9, 'M': 1e6, 'k': 1e3, 'da': 10, 'd': 1e-1, + 'c': 1e-2, 'm': 0.001, u'µ': 1e-6, 'u': 1e-6, 'n': 1e-9, 'p': 1e-12, 'f': 1e-15, 'a': 1e-18, +} + + +def frequency(value): + value = string(value).replace(' ', '').lower() + if value.endswith('Hz') or value.endswith('hz') or value.endswith('HZ'): + value = value[:-2] + if not value: + raise vol.Invalid(u"Frequency must have value") + multiplier = 1 + if value[:-1] in METRIC_SUFFIXES: + multiplier = METRIC_SUFFIXES[value[:-1]] + value = value[:-1] + elif len(value) >= 2 and value[:-2] in METRIC_SUFFIXES: + multiplier = METRIC_SUFFIXES[value[:-2]] + value = value[:-2] + float_val = vol.Coerce(float)(value) + return float_val * multiplier + + +def hostname(value): + value = string(value) + if len(value) > 63: + raise vol.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 -") + return value + + +def ssid(value): + if value is None: + raise vol.Invalid("SSID can not be None") + if not isinstance(value, str): + 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") + return value + + +def ipv4(value): + if isinstance(value, list): + parts = value + elif isinstance(value, str): + parts = value.split('.') + else: + raise vol.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") + 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") + return IPAddress(*parts_) + + +def publish_topic(value): + value = string_strict(value) + if value.endswith('/'): + raise vol.Invalid("Publish topic can't end with '/'") + return value + + +subscribe_topic = string_strict # TODO improve this +mqtt_payload = string # TODO improve this +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)) +i2c_address = hex_uint8_t + + +def invalid(value): + raise vol.Invalid("This shouldn't happen.") + + +def valid(value): + return value + + +REGISTERED_IDS = set() + + +def register_variable_id(value): + s = variable_id(value) + if s in REGISTERED_IDS: + raise vol.Invalid("This ID has already been used") + REGISTERED_IDS.add(s) + return s + + +class GenerateID(vol.Optional): + def __init__(self, basename): + self._basename = basename + super(GenerateID, self).__init__(CONF_ID, default=self.default_variable_id) + + def default_variable_id(self): + return ensure_unique_string(self._basename, REGISTERED_IDS) + + +ID_SCHEMA = vol.Schema({ + vol.Required(CONF_ID): invalid, +}) + +REQUIRED_ID_SCHEMA = vol.Schema({ + vol.Required(CONF_ID): register_variable_id, +}) + +PLATFORM_SCHEMA = ID_SCHEMA.extend({ + vol.Required(CONF_PLATFORM): valid, +}) + +MQTT_COMPONENT_AVAILABILITY_SCHEMA = vol.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, +}) + +MQTT_COMPONENT_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): string, + vol.Optional(CONF_RETAIN): boolean, + vol.Optional(CONF_DISCOVERY): boolean, + vol.Optional(CONF_STATE_TOPIC): publish_topic, + vol.Optional(CONF_AVAILABILITY): MQTT_COMPONENT_AVAILABILITY_SCHEMA, +}) + +MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend({ + vol.Optional(CONF_COMMAND_TOPIC): subscribe_topic, +}) diff --git a/esphomeyaml/const.py b/esphomeyaml/const.py new file mode 100644 index 0000000000..b25e4ac8b4 --- /dev/null +++ b/esphomeyaml/const.py @@ -0,0 +1,175 @@ +"""Constants used by esphomeyaml.""" + +MAJOR_VERSION = 0 +MINOR_VERSION = 1 +PATCH_VERSION = '0' +__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) +__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) + +ESP_PLATFORM_ESP32 = 'ESP32' +ESP_PLATFORM_ESP8266 = 'ESP8266' +ESP_PLATFORMS = [ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266] + +APB_CLOCK_FREQ = 80000000 + +CONF_ESPHOMEYAML = 'esphomeyaml' +CONF_NAME = 'name' +CONF_PLATFORM = 'platform' +CONF_BOARD = 'board' +CONF_SIMPLIFY = 'simplify' +CONF_LIBRARY_URI = 'library_uri' +CONF_LOGGER = 'logger' +CONF_WIFI = 'wifi' +CONF_SSID = 'ssid' +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_POWER_SUPPLY = 'power_supply' +CONF_ID = 'id' +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_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_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_PAYLOAD = 'payload' +CONF_QOS = 'qos' +CONF_DISCOVERY_RETAIN = 'discovery_retain' +CONF_TOPIC_PREFIX = 'topic_prefix' +CONF_HOSTNAME = 'hostname' +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_FILTER_NAN = 'filter_nan' +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_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_METER = 'timeout_meter' +CONF_TIMEOUT_TIME = 'timeout_time' +CONF_IR_TRANSMITTER_ID = 'ir_transmitter_id' +CONF_CARRIER_DUTY_PERCENT = 'carrier_duty_percent' +CONF_NEC = 'nec' +CONF_COMMAND = 'command' +CONF_DATA = 'data' +CONF_NBITS = 'nbits' +CONF_LG = 'lg' +CONF_SONY = 'sony' +CONF_PANASONIC = 'panasonic' +CONF_REPEAT = 'repeat' +CONF_TIMES = 'times' +CONF_WAIT_TIME_US = 'wait_time_us' +CONF_OSCILLATION_OUTPUT = 'oscillation_output' +CONF_SPEED = 'speed' +CONF_OSCILLATION_STATE_TOPIC = 'oscillation_state_topic' +CONF_OSCILLATION_COMMAND_TOPIC = 'oscillation_command_topic' +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' + +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 +} diff --git a/esphomeyaml/core.py b/esphomeyaml/core.py new file mode 100644 index 0000000000..4ac7300fed --- /dev/null +++ b/esphomeyaml/core.py @@ -0,0 +1,18 @@ +class ESPHomeYAMLError(Exception): + """General esphomeyaml exception occurred.""" + pass + + +class HexInt(long): + def __str__(self): + return "0x{:X}".format(self) + + +class IPAddress(object): + def __init__(self, *args): + if len(args) != 4: + raise ValueError(u"IPAddress must consist up 4 items") + self.args = args + + def __str__(self): + return '.'.join(str(x) for x in self.args) diff --git a/esphomeyaml/espota.py b/esphomeyaml/espota.py new file mode 100755 index 0000000000..09a536759a --- /dev/null +++ b/esphomeyaml/espota.py @@ -0,0 +1,331 @@ +#!/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 +import optparse +import os +import random +import socket +import sys + +# 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: + barLength = 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(barLength * progress)) + text = "\rUploading: [{0}] {1}% {2}".format("=" * block + " " * (barLength - 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: + _LOGGER.error("Listen Failed") + return 1 + + content_size = os.path.getsize(filename) + f = open(filename, 'rb') + file_md5 = hashlib.md5(f.read()).hexdigest() + f.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: + _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: + 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: + _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, client_address = sock.accept() + sock.settimeout(None) + connection.settimeout(None) + except Exception: + _LOGGER.error('No response from device') + sock.close() + return 1 + + try: + f = open(filename, "rb") + if PROGRESS: + update_progress(0) + else: + _LOGGER.info('Uploading...') + offset = 0 + while True: + chunk = f.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: + sys.stderr.write('\n') + _LOGGER.error('Error Uploading') + connection.close() + f.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.close() + sock.close() + if data != "OK": + _LOGGER.error('%s', data) + return 1 + except Exception: + _LOGGER.error('No Result!') + connection.close() + f.close() + sock.close() + return 1 + + finally: + connection.close() + f.close() + + return 0 + + +def parser(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, args) = parser.parse_args(unparsed_args) + + return options + + +def main(args): + options = parser(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/esphomeyaml/helpers.py b/esphomeyaml/helpers.py new file mode 100644 index 0000000000..cc34d62469 --- /dev/null +++ b/esphomeyaml/helpers.py @@ -0,0 +1,400 @@ +from __future__ import print_function + +import logging +import re +from collections import OrderedDict, deque + +from esphomeyaml.const import CONF_AVAILABILITY, CONF_COMMAND_TOPIC, CONF_DISCOVERY, \ + CONF_INVERTED, \ + CONF_MODE, CONF_NUMBER, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_RETAIN, \ + CONF_STATE_TOPIC, CONF_TOPIC +from esphomeyaml.core import ESPHomeYAMLError, HexInt + +_LOGGER = logging.getLogger(__name__) + +SIMPLIFY = False + + +def ensure_unique_string(preferred_string, current_strings): + test_string = preferred_string + current_strings_set = set(current_strings) + + tries = 1 + + while test_string in current_strings_set: + tries += 1 + test_string = u"{}_{}".format(preferred_string, tries) + + return test_string + + +def indent_all_but_first_and_last(text, padding=u' '): + lines = text.splitlines(True) + if len(lines) <= 2: + return text + return lines[0] + u''.join(padding + line for line in lines[1:-1]) + lines[-1] + + +def indent_list(text, padding=u' '): + return [padding + line for line in text.splitlines()] + + +def indent(text, padding=u' '): + return u'\n'.join(indent_list(text, padding)) + + +class Expression(object): + def __init__(self): + pass + + def __str__(self): + raise NotImplemented + + +class RawExpression(Expression): + def __init__(self, text): + super(RawExpression, self).__init__() + self.text = text + + def __str__(self): + return self.text + + +class AssignmentExpression(Expression): + def __init__(self, lhs, rhs, obj): + super(AssignmentExpression, self).__init__() + self.obj = obj + self.lhs = safe_exp(lhs) + self.rhs = safe_exp(rhs) + + def __str__(self): + return u"{} = {}".format(self.lhs, self.rhs) + + +class ExpressionList(Expression): + def __init__(self, *args): + super(ExpressionList, self).__init__() + # Remove every None on end + args = list(args) + while args and args[-1] is None: + args.pop() + self.args = [safe_exp(x) for x in args] + + def __str__(self): + text = u", ".join(unicode(x) for x in self.args) + return indent_all_but_first_and_last(text) + + +class CallExpression(Expression): + def __init__(self, base, *args): + super(CallExpression, self).__init__() + self.base = base + self.args = ExpressionList(*args) + + def __str__(self): + return u'{}({})'.format(self.base, self.args) + + +class StructInitializer(Expression): + def __init__(self, base, *args): + super(StructInitializer, self).__init__() + self.base = base + if not isinstance(args, OrderedDict): + args = OrderedDict(args) + self.args = OrderedDict() + for key, value in args.iteritems(): + if value is not None: + self.args[key] = safe_exp(value) + + def __str__(self): + s = u'{}{{\n'.format(self.base) + for key, value in self.args.iteritems(): + s += u' .{} = {},\n'.format(key, value) + s += u'}' + return s + + +class ArrayInitializer(Expression): + def __init__(self, *args): + super(ArrayInitializer, self).__init__() + self.args = [safe_exp(x) for x in args if x is not None] + + def __str__(self): + if not self.args: + return u'{}' + s = u'{\n' + for arg in self.args: + s += u' {},\n'.format(arg) + s += u'}' + return s + + +class Literal(Expression): + def __init__(self): + super(Literal, self).__init__() + + +class StringLiteral(Literal): + def __init__(self, s): + super(StringLiteral, self).__init__() + self.s = s + + def __str__(self): + return u'"{}"'.format(self.s) + + +class IntLiteral(Literal): + def __init__(self, i): + super(IntLiteral, self).__init__() + self.i = i + + def __str__(self): + return unicode(self.i) + + +class BoolLiteral(Literal): + def __init__(self, b): + super(BoolLiteral, self).__init__() + self.b = b + + def __str__(self): + return u"true" if self.b else u"false" + + +class HexIntLiteral(Literal): + def __init__(self, i): + super(HexIntLiteral, self).__init__() + self.i = HexInt(i) + + def __str__(self): + return str(self.i) + + +class FloatLiteral(Literal): + def __init__(self, f): + super(FloatLiteral, self).__init__() + self.f = f + + def __str__(self): + return u"{:f}f".format(self.f) + + +def safe_exp(obj): + if isinstance(obj, Expression): + return obj + elif isinstance(obj, bool): + return BoolLiteral(obj) + elif isinstance(obj, str) or isinstance(obj, unicode): + return StringLiteral(obj) + elif isinstance(obj, (int, long)): + return IntLiteral(obj) + elif isinstance(obj, float): + return FloatLiteral(obj) + raise ValueError(u"Object is not an expression", obj) + + +class Statement(object): + def __init__(self): + pass + + def __str__(self): + raise NotImplemented + + +class RawStatement(Statement): + def __init__(self, text): + super(RawStatement, self).__init__() + self.text = text + + def __str__(self): + return self.text + + +class ExpressionStatement(Statement): + def __init__(self, expression): + super(ExpressionStatement, self).__init__() + self.expression = safe_exp(expression) + + def __str__(self): + return u"{};".format(self.expression) + + +def statement(expression): + if isinstance(expression, Statement): + return expression + return ExpressionStatement(expression) + + +def variable(type, id, rhs): + lhs = RawExpression(u'{} {}'.format(type if not SIMPLIFY else u'auto', id)) + rhs = safe_exp(rhs) + obj = MockObj(id, u'.') + add(AssignmentExpression(lhs, rhs, obj)) + _VARIABLES[id] = obj, type + return obj + + +def Pvariable(type, id, rhs): + lhs = RawExpression(u'{} *{}'.format(type if not SIMPLIFY else u'auto', id)) + rhs = safe_exp(rhs) + obj = MockObj(id, u'->') + add(AssignmentExpression(lhs, rhs, obj)) + _VARIABLES[id] = obj, type + return obj + + +_QUEUE = deque() +_VARIABLES = {} +_EXPRESSIONS = [] + + +def get_variable(id, type=None): + result = None + while _QUEUE: + if id is not None: + if id in _VARIABLES: + result = _VARIABLES[id][0] + break + elif type is not None: + result = next((x[0] for x in _VARIABLES.itervalues() if x[1] == type), None) + if result is not None: + break + func, config = _QUEUE.popleft() + func(config) + if id is None and type is None: + return None + if result is None: + if id is not None: + result = _VARIABLES[id][0] + elif type is not None: + result = next((x[0] for x in _VARIABLES.itervalues() if x[1] == type), None) + + if result is None: + raise ESPHomeYAMLError(u"Couldn't find ID '{}' with type {}".format(id, type)) + result.usages += 1 + return result + + +def add_task(func, config): + _QUEUE.append((func, config)) + + +def add(expression): + _EXPRESSIONS.append(expression) + return expression + + +class MockObj(Expression): + def __init__(self, base, op=u'.', parent=None): + self.base = base + self.op = op + self.usages = 0 + self.parent = parent + super(MockObj, self).__init__() + + def __getattr__(self, attr): + next_op = u'.' + if attr.startswith(u'P'): + attr = attr[1:] + next_op = u'->' + op = self.op + return MockObj(u'{}{}{}'.format(self.base, op, attr), next_op, self) + + def __call__(self, *args, **kwargs): + self.usages += 1 + it = self.parent + while it is not None: + it.usages += 1 + it = it.parent + return CallExpression(self.base, *args) + + def __str__(self): + return self.base + + +App = MockObj(u'App') + +GPIOPin = MockObj(u'GPIOPin') +GPIOOutputPin = MockObj(u'GPIOOutputPin') +GPIOInputPin = MockObj(u'GPIOInputPin') + + +def get_gpio_pin_number(conf): + if isinstance(conf, int): + return conf + return conf[CONF_NUMBER] + + +def exp_gpio_pin_(obj, conf, default_mode): + if isinstance(conf, int): + return conf + if conf.get(CONF_INVERTED) is None: + return obj(conf[CONF_NUMBER], conf.get(CONF_MODE)) + return obj(conf[CONF_NUMBER], RawExpression(conf.get(CONF_MODE, default_mode)), + conf[CONF_INVERTED]) + + +def exp_gpio_pin(conf): + return GPIOPin(conf[CONF_NUMBER], conf[CONF_MODE], conf.get(CONF_INVERTED)) + + +def exp_gpio_output_pin(conf): + return exp_gpio_pin_(GPIOOutputPin, conf, u'OUTPUT') + + +def exp_gpio_input_pin(conf): + return exp_gpio_pin_(GPIOInputPin, conf, u'INPUT') + + +def setup_mqtt_component(obj, config): + if CONF_RETAIN in config: + add(obj.set_retain(config[CONF_RETAIN])) + if not config.get(CONF_DISCOVERY, True): + add(obj.disable_discovery()) + if CONF_STATE_TOPIC in config: + add(obj.set_custom_state_topic(config[CONF_STATE_TOPIC])) + if CONF_COMMAND_TOPIC in config: + add(obj.set_custom_command_topic(config[CONF_COMMAND_TOPIC])) + if CONF_AVAILABILITY in config: + availability = config[CONF_AVAILABILITY] + exp = StructInitializer( + u'mqtt::Availability', + (u'topic', availability[CONF_TOPIC]), + (u'payload_available', availability[CONF_PAYLOAD_AVAILABLE]), + (u'payload_not_available', availability[CONF_PAYLOAD_NOT_AVAILABLE]), + ) + add(obj.set_availability(exp)) + + +def exp_empty_optional(type): + return RawExpression(u'Optional<{}>()'.format(type)) + + +def exp_optional(type, value): + if value is None: + return exp_empty_optional(type) + return value + + +# shlex's quote for Python 2.7 +_find_unsafe = re.compile(r'[^\w@%+=:,./-]').search + + +def quote(s): + """Return a shell-escaped version of the string *s*.""" + if not s: + return u"''" + if _find_unsafe(s) is None: + return s + + # use single quotes, and put single quotes into double quotes + # the string $'b is then quoted as '$'"'"'b' + return u"'" + s.replace(u"'", u"'\"'\"'") + u"'" + + +def color(the_color, message = '', reset=None): + """Color helper.""" + from colorlog.escape_codes import escape_codes, parse_colors + if not message: + return parse_colors(the_color) + return parse_colors(the_color) + message + escape_codes[reset or 'reset'] diff --git a/esphomeyaml/mqtt.py b/esphomeyaml/mqtt.py new file mode 100644 index 0000000000..31d4272629 --- /dev/null +++ b/esphomeyaml/mqtt.py @@ -0,0 +1,69 @@ +from __future__ import print_function + +import logging +from datetime import datetime + +import paho.mqtt.client as mqtt + +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, \ + CONF_USERNAME + +_LOGGER = logging.getLogger(__name__) + + +def initialize(config, subscriptions, on_message, username, password, client_id): + def on_connect(client, userdata, flags, rc): + for topic in subscriptions: + client.subscribe(topic) + + client = mqtt.Client(client_id or u'') + client.on_connect = on_connect + client.on_message = on_message + if username is None: + if config[CONF_MQTT].get(CONF_USERNAME): + client.username_pw_set(config[CONF_MQTT][CONF_USERNAME], + config[CONF_MQTT][CONF_PASSWORD]) + elif username: + client.username_pw_set(username, password) + client.connect(config[CONF_MQTT][CONF_BROKER], config[CONF_MQTT][CONF_PORT]) + + try: + client.loop_forever() + except KeyboardInterrupt: + pass + return 0 + + +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_LOGGER, {}): + topic = config[CONF_LOGGER][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' + _LOGGER.info(u"Starting log output from %s", topic) + + def on_message(client, userdata, msg): + t = datetime.now().time().strftime(u'[%H:%M:%S] ') + print(t + msg.payload) + + return initialize(config, [topic], on_message, username, password, client_id) + + +def clear_topic(config, topic, username=None, password=None, client_id=None): + if topic is None: + discovery_prefix = config[CONF_MQTT].get(CONF_DISCOVERY_PREFIX, u'homeassistant') + name = config[CONF_ESPHOMEYAML][CONF_NAME] + topic = u'{}/+/{}/#'.format(discovery_prefix, name) + _LOGGER.info(u"Clearing messages from {}".format(topic)) + + def on_message(client, userdata, msg): + if not msg.payload: + return + print(u"Clearing topic {}".format(msg.topic)) + client.publish(msg.topic, None, retain=True) + + return initialize(config, [topic], on_message, username, password, client_id) diff --git a/esphomeyaml/pins.py b/esphomeyaml/pins.py new file mode 100644 index 0000000000..f3c26d21bd --- /dev/null +++ b/esphomeyaml/pins.py @@ -0,0 +1,196 @@ +import logging + +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.const import ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266, CONF_NUMBER, CONF_MODE, \ + CONF_INVERTED + +_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': 0, '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 +} + +ESP32_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, + '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, +} + + +def _translate_pin(value): + if isinstance(value, dict) or value is None: + raise vol.Invalid(u"This option doesn't allow more complicated options like inverted.") + if isinstance(value, int): + return value + try: + return int(value) + except ValueError: + pass + if value.startswith('GPIO'): + return vol.Coerce(int)(value[len('GPIO'):]) + if cv.ESP_PLATFORM == ESP_PLATFORM_ESP32: + if value in ESP32_PINS: + return ESP32_PINS[value] + if cv.BOARD not in ESP32_BOARD_TO_PINS: + raise vol.Invalid(u"ESP32: Unknown board {} with unknown " + u"pin {}.".format(cv.BOARD, value)) + if value not in ESP32_BOARD_TO_PINS[cv.BOARD]: + raise vol.Invalid(u"ESP32: Board {} doesn't have" + u"pin {}".format(cv.BOARD, value)) + return ESP32_BOARD_TO_PINS[cv.BOARD][value] + elif cv.ESP_PLATFORM == ESP_PLATFORM_ESP8266: + if value in ESP8266_PINS: + return ESP8266_PINS[value] + if cv.BOARD not in ESP8266_BOARD_TO_PINS: + raise vol.Invalid(u"ESP8266: Unknown board {} with unknown " + u"pin {}.".format(cv.BOARD, value)) + if value not in ESP8266_BOARD_TO_PINS[cv.BOARD]: + raise vol.Invalid(u"ESP8266: Board {} doesn't have" + u"pin {}".format(cv.BOARD, value)) + return ESP8266_BOARD_TO_PINS[cv.BOARD][value] + raise vol.Invalid(u"Invalid ESP platform.") + + +def _validate_gpio_pin(value): + value = _translate_pin(value) + if cv.ESP_PLATFORM == ESP_PLATFORM_ESP32: + if value < 0 or value > 39: + raise vol.Invalid(u"ESP32: Invalid pin number: {}".format(value)) + if 6 <= value <= 11: + _LOGGER.warning(u"ESP32: Pin {} (6-11) might already be used by the " + u"flash interface. Be warned.".format(value)) + if value in (20, 24, 28, 29, 30, 31): + _LOGGER.warning(u"ESP32: Pin {} (20, 24, 28-31) can usually not be used. " + u"Be warned.".format(value)) + return value + elif cv.ESP_PLATFORM == ESP_PLATFORM_ESP8266: + if 6 <= value <= 11: + _LOGGER.warning(u"ESP8266: Pin {} (6-11) might already be used by the " + u"flash interface. Be warned.".format(value)) + if value < 0 or value > 17: + raise vol.Invalid(u"ESP8266: Invalid pin number: {}".format(value)) + return value + raise vol.Invalid(u"Invalid ESP platform.") + + +def input_pin(value): + value = _validate_gpio_pin(value) + if cv.ESP_PLATFORM == ESP_PLATFORM_ESP32: + return value + elif cv.ESP_PLATFORM == ESP_PLATFORM_ESP8266: + return value + raise vol.Invalid(u"Invalid ESP platform.") + + +def output_pin(value): + value = _validate_gpio_pin(value) + if cv.ESP_PLATFORM == ESP_PLATFORM_ESP32: + if 34 <= value <= 39: + raise vol.Invalid(u"ESP32: Pin {} (34-39) can only be used as " + u"input pins.".format(value)) + return value + elif cv.ESP_PLATFORM == ESP_PLATFORM_ESP8266: + if value == 16: + raise vol.Invalid(u"Pin {} doesn't support output mode".format(value)) + return value + raise vol.Invalid("Invalid ESP platform.") + + +def analog_pin(value): + value = _validate_gpio_pin(value) + if cv.ESP_PLATFORM == ESP_PLATFORM_ESP32: + if 32 <= value <= 39: # ADC1 + return value + raise vol.Invalid(u"ESP32: Only pins 32 though 39 support ADC.") + elif cv.ESP_PLATFORM == ESP_PLATFORM_ESP8266: + if value == 17: # A0 + return value + raise vol.Invalid(u"ESP8266: Only pin A0 (17) supports ADC.") + raise vol.Invalid(u"Invalid ESP platform.") + + +input_output_pin = vol.All(input_pin, output_pin) +gpio_pin = vol.Any(input_pin, output_pin) +PIN_MODES_ESP8266 = [ + 'INPUT', 'OUTPUT', 'INPUT_PULLUP', 'OUTPUT_OPEN_DRAIN', 'SPECIAL', 'FUNCTION_1', + 'FUNCTION_2', 'FUNCTION_3', 'FUNCTION_4', + 'FUNCTION_0', 'WAKEUP_PULLUP', 'WAKEUP_PULLDOWN', 'INPUT_PULLDOWN_16', +] +PIN_MODES_ESP32 = [ + 'INPUT', 'OUTPUT', 'INPUT_PULLUP', 'OUTPUT_OPEN_DRAIN', 'SPECIAL', 'FUNCTION_1', + 'FUNCTION_2', 'FUNCTION_3', 'FUNCTION_4', + 'PULLUP', 'PULLDOWN', 'INPUT_PULLDOWN', 'OPEN_DRAIN', 'FUNCTION_5', + 'FUNCTION_6', 'ANALOG', +] + + +def pin_mode(value): + value = vol.All(vol.Coerce(str), vol.Upper)(value) + if cv.ESP_PLATFORM == ESP_PLATFORM_ESP32: + return vol.Any(*PIN_MODES_ESP32)(value) + elif cv.ESP_PLATFORM == ESP_PLATFORM_ESP8266: + return vol.Any(*PIN_MODES_ESP8266)(value) + raise vol.Invalid(u"Invalid ESP platform.") + + +GPIO_PIN_SCHEMA = vol.Schema({ + vol.Required(CONF_NUMBER): gpio_pin, + vol.Required(CONF_MODE): pin_mode, + vol.Optional(CONF_INVERTED): cv.boolean, +}) + +GPIO_OUTPUT_PIN_SCHEMA = vol.Any(output_pin, vol.Schema({ + vol.Required(CONF_NUMBER): output_pin, + vol.Optional(CONF_MODE): pin_mode, + vol.Optional(CONF_INVERTED): cv.boolean, +})) + +GPIO_INPUT_PIN_SCHEMA = vol.Any(input_pin, vol.Schema({ + vol.Required(CONF_NUMBER): input_pin, + vol.Optional(CONF_MODE): pin_mode, + vol.Optional(CONF_INVERTED): cv.boolean, +})) diff --git a/esphomeyaml/wizard.py b/esphomeyaml/wizard.py new file mode 100644 index 0000000000..2f3a078383 --- /dev/null +++ b/esphomeyaml/wizard.py @@ -0,0 +1,293 @@ +from __future__ import print_function + +import codecs +import os +from time import sleep +import unicodedata + +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.components import mqtt +from esphomeyaml.const import ESP_PLATFORMS, ESP_PLATFORM_ESP32, ESP_BOARDS_FOR_PLATFORM +from esphomeyaml.helpers import color + +CORE_BIG = """ _____ ____ _____ ______ + / ____/ __ \| __ \| ____| + | | | | | | |__) | |__ + | | | | | | _ /| __| + | |___| |__| | | \ \| |____ + \_____\____/|_| \_\______| +""" +ESP_BIG = """ ______ _____ _____ + | ____|/ ____| __ \ + | |__ | (___ | |__) | + | __| \___ \| ___/ + | |____ ____) | | + |______|_____/|_| +""" +WIFI_BIG = """ __ ___ ______ _ + \ \ / (_) ____(_) + \ \ /\ / / _| |__ _ + \ \/ \/ / | | __| | | + \ /\ / | | | | | + \/ \/ |_|_| |_| +""" +MQTT_BIG = """ __ __ ____ _______ _______ + | \/ |/ __ \__ __|__ __| + | \ / | | | | | | | | + | |\/| | | | | | | | | + | | | | |__| | | | | | + |_| |_|\___\_\ |_| |_| +""" +OTA_BIG = """ ____ _______ + / __ \__ __|/\ + | | | | | | / \ + | | | | | | / /\ \ + | |__| | | |/ ____ \ + \____/ |_/_/ \_\\ +""" + +# TODO handle escaping +BASE_CONFIG = """esphomeyaml: + name: {name} + platform: {platform} + board: {board} + +wifi: + ssid: '{ssid}' + password: '{psk}' + +mqtt: + broker: '{broker}' + username: '{mqtt_username}' + password: '{mqtt_password}' + +# Enable logging +logger: + +""" + + +def print_step(step, big): + print() + print() + print("============= STEP {} =============".format(step)) + print(big) + print("===================================") + sleep(0.25) + + +def default_input(text, default): + print() + print("Press ENTER for default ({})".format(default)) + return raw_input(text.format(default)) or default + + +# From https://stackoverflow.com/a/518232/8924614 +def strip_accents(s): + return u''.join(c for c in unicodedata.normalize('NFD', unicode(s)) + if unicodedata.category(c) != 'Mn') + + +def wizard(path): + if not path.endswith('.yaml') and not path.endswith('.yml'): + print("Please make your configuration file {} have the extension .yaml or .yml".format( + color('cyan', path))) + return 1 + if os.path.exists(path): + print("Uh oh, it seems like {} already exists, please delete that file first " + "or chose another configuration file.".format(color('cyan', path))) + return 1 + print("Hi there!") + sleep(1.5) + print("I'm the wizard of esphomeyaml :)") + sleep(1.25) + print("And I'm here to help you get started with esphomeyaml.") + sleep(2.0) + print("In 5 steps I'm going to guide you through creating a basic " + "configuration file for your custom ESP8266/ESP32 firmware. Yay!") + sleep(3.0) + print() + print_step(1, CORE_BIG) + print("First up, please choose a " + color('green', 'name') + " for your node.") + print("It should be a unique name that can be used to identify the device later.") + sleep(1) + print("For example, I like calling the node in my living room {}.".format( + color('bold_white', "livingroom"))) + print() + sleep(1) + name = raw_input(color("bold_white", "(name): ")) + while True: + try: + name = cv.valid_name(name) + break + except vol.Invalid: + print(color("red", "Oh noes, \"{}\" isn't a valid name. Names can only include " + "numbers, letters and underscores.".format(name))) + name = strip_accents(name).replace(' ', '_') + name = u''.join(c for c in name if c in cv.ALLOWED_NAME_CHARS) + print("Shall I use \"{}\" as the name instead?".format(color('cyan', name))) + sleep(0.5) + name = default_input("(name [{}]): ", name) + + print("Great! Your node is now called \"{}\".".format(color('cyan', name))) + sleep(1) + print_step(2, ESP_BIG) + print("Now I'd like to know which *board* you're using so that I can compile " + "firmwares for it.") + print("Are you using an " + color('green', 'ESP32') + " or " + + color('green', 'ESP8266') + " based board?") + while True: + sleep(0.5) + print() + print("Please enter either ESP32 or ESP8266.") + platform = raw_input(color("bold_white", "(ESP32/ESP8266): ")) + try: + platform = vol.All(vol.Upper, vol.Any(*ESP_PLATFORMS))(platform) + break + except vol.Invalid: + print("Unfortunately, I can't find an espressif microcontroller called " + "\"{}\". Please try again.".format(platform)) + print("Thanks! You've chosen {} as your platform.".format(color('cyan', platform))) + print() + sleep(1) + + if platform == ESP_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' + + print("Next, I need to know what " + color('green', 'board') + " you're using.") + sleep(0.5) + print("Please go to {} and choose a board.".format(color('green', board_link))) + print() + # Don't sleep because user needs to copy link + if platform == ESP_PLATFORM_ESP32: + print("For example \"{}\".".format(color("bold_white", 'nodemcu-32s'))) + else: + print("For example \"{}\".".format(color("bold_white", 'nodemcuv2'))) + 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 + except vol.Invalid: + print(color('red', "Sorry, I don't think the board \"{}\" exists.")) + print() + sleep(0.25) + print("Possible options are {}".format(', '.join(boards))) + print() + + print("Way to go! You've chosen {} as your board.".format(color('cyan', board))) + print() + sleep(1) + + print_step(3, WIFI_BIG) + print("In this step, I'm going to create the configuration for " + "WiFi.") + print() + sleep(1) + print("First, what's the " + color('green', 'SSID') + " (the name) of the WiFi network {} " + "I should connect to?".format(name)) + sleep(1.5) + print("For example \"{}\".".format(color('bold_white', "Abraham Linksys"))) + while True: + ssid = raw_input(color('bold_white', "(ssid): ")) + try: + ssid = cv.ssid(ssid) + break + except vol.Invalid: + print(color('red', "Unfortunately, \"{}\" doesn't seem to be a valid SSID. " + "Please try again.".format(ssid))) + print() + sleep(1) + + print("Thank you very much! You've just chosen \"{}\" as your SSID.".format( + color('cyan', ssid))) + print() + sleep(0.75) + + print("Now please state the " + color('green', 'password') + + " of the WiFi network so that I can connect to it.") + print() + print("For example \"{}\"".format(color('bold_white', 'PASSWORD42'))) + sleep(0.5) + psk = raw_input(color('bold_white', '(PSK): ')) + print("Perfect! WiFi is now set up (you can create static IPs and so on later).") + sleep(1.5) + + print_step(4, MQTT_BIG) + print("Almost there! Now let's setup MQTT so that your node can connect to the outside world.") + print() + sleep(1) + print("Please enter the " + color('green', 'address') + " of your MQTT broker.") + print() + print("For example \"{}\".".format(color('bold_white', '192.168.178.84'))) + while True: + broker = raw_input(color('bold_white', "(broker): ")) + try: + broker = mqtt.validate_broker(broker) + break + except vol.Invalid as e: + print(color('red', "The broker address \"{}\" seems to be invalid: {} :(".format( + broker, e))) + print("Please try again.") + print() + sleep(1) + + print("Thanks! Now enter the " + color('green', 'username') + " and " + + color('green', 'password') + " for the MQTT broker. Leave empty for no authentication.") + mqtt_username = raw_input(color('bold_white', '(username): ')) + mqtt_password = '' + if mqtt_username: + mqtt_password = raw_input(color('bold_white', '(password): ')) + + show = '*' * len(mqtt_password) + if len(mqtt_password) >= 2: + show = mqtt_password[:2] + '*' * len(mqtt_password) + print("MQTT Username: \"{}\"; Password: \"{}\"".format( + color('cyan', mqtt_username), color('cyan', show))) + else: + print("No authentication for MQTT") + + print_step(5, OTA_BIG) + print("Last step! esphomeyaml can automatically upload custom firmwares over WiFi " + "(over the air).") + print("This can be insecure if you do not trust the WiFi network. Do you want to set " + "an " + color('green', 'OTA password') + " for remote updates?") + print() + sleep(0.25) + print("Press ENTER for no password") + ota_password = raw_input(color('bold_white', '(password): ')) + + config = BASE_CONFIG.format(name=name, platform=platform, board=board, + ssid=ssid, psk=psk, broker=broker, + mqtt_username=mqtt_username, mqtt_password=mqtt_password) + + if ota_password: + config += "ota:\n password: '{}'".format(ota_password) + else: + config += "ota:\n" + + with codecs.open(path, 'w') as f: + f.write(config) + + print() + print(color('cyan', "DONE! I've now written a new configuration file to ") + + color('bold_cyan', path)) + print() + print("Next steps:") + print(" > If you haven't already, enable MQTT discovery in Home Assistant:") + print() + print(color('bold_white', "# In your configuration.yaml")) + print(color('bold_white', "mqtt:")) + print(color('bold_white', " broker: {}".format(broker))) + print(color('bold_white', " # ...")) + print(color('bold_white', " discovery: True")) + print() + print(" > Then follow the rest of the getting started guide:") + print(" > https://esphomelib.com/esphomeyaml/getting-started.html") + return 0 + diff --git a/esphomeyaml/writer.py b/esphomeyaml/writer.py new file mode 100644 index 0000000000..9171fa0268 --- /dev/null +++ b/esphomeyaml/writer.py @@ -0,0 +1,149 @@ +from __future__ import print_function + +import codecs +import errno +import os + +from esphomeyaml.config import get_component +from esphomeyaml.const import CONF_BOARD, CONF_ESPHOMEYAML, CONF_LIBRARY_URI, CONF_LOGGER, \ + CONF_NAME, CONF_OTA, CONF_PLATFORM, ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266 +from esphomeyaml.core import ESPHomeYAMLError + +CPP_AUTO_GENERATE_BEGIN = u'// ========== AUTO GENERATED CODE BEGIN ===========' +CPP_AUTO_GENERATE_END = u'// =========== AUTO GENERATED CODE END ============' +INI_AUTO_GENERATE_BEGIN = u'; ========== AUTO GENERATED CODE BEGIN ===========' +INI_AUTO_GENERATE_END = u'; =========== AUTO GENERATED CODE END ============' + +CPP_BASE_FORMAT = (u"""// Auto generated code by esphomeyaml +#include "esphomelib/application.h" + +using namespace esphomelib; + +void setup() { + // ===== DO NOT EDIT ANYTHING BELOW THIS LINE ===== + """, u""" + // ========= YOU CAN EDIT AFTER THIS LINE ========= + App.setup(); +} + +void loop() { + App.loop(); + delay(1); +} +""") + +INI_BASE_FORMAT = (u"""; Auto generated code by esphomeyaml + +[common] +lib_deps = +build_flags = +upload_flags = + +; ===== DO NOT EDIT ANYTHING BELOW THIS LINE ===== +""", u""" +; ========= YOU CAN EDIT AFTER THIS LINE ========= + +""") + +INI_CONTENT_FORMAT = u"""[env:{env}] +platform = {platform} +board = {board} +framework = arduino +lib_deps = + {esphomeyaml_uri} + ${{common.lib_deps}} +build_flags ={build_flags} + ${{common.build_flags}} +""" + +PLATFORM_TO_PLATFORMIO = { + ESP_PLATFORM_ESP32: 'espressif32', + ESP_PLATFORM_ESP8266: 'espressif8266' +} + + +def get_ini_content(config): + d = { + u'env': config[CONF_ESPHOMEYAML][CONF_NAME], + u'platform': PLATFORM_TO_PLATFORMIO[config[CONF_ESPHOMEYAML][CONF_PLATFORM]], + u'board': config[CONF_ESPHOMEYAML][CONF_BOARD], + u'esphomeyaml_uri': config[CONF_ESPHOMEYAML][CONF_LIBRARY_URI], + u'build_flags': u'', + } + if CONF_LOGGER in config: + build_flags = get_component(CONF_LOGGER).get_build_flags(config[CONF_LOGGER]) + if build_flags: + d[u'build_flags'] = u'\n ' + build_flags + return INI_CONTENT_FORMAT.format(**d) + + +def mkdir_p(path): + try: + os.makedirs(path) + except OSError as exc: # Python >2.5 + if exc.errno == errno.EEXIST and os.path.isdir(path): + pass + else: + raise + + +def find_begin_end(text, begin_s, end_s): + begin_index = text.find(begin_s) + if begin_index == -1: + raise ESPHomeYAMLError(u"Could not find auto generated code begin in file, either" + u"delete the main sketch file or insert the comment again.") + if text.find(begin_s, begin_index + 1) != -1: + raise ESPHomeYAMLError(u"Found multiple auto generate code begins, don't know" + u"which to chose, please remove one of them.") + end_index = text.find(end_s) + if end_index == -1: + raise ESPHomeYAMLError(u"Could not find auto generated code end in file, either" + u"delete the main sketch file or insert the comment again.") + if text.find(end_s, end_index + 1) != -1: + raise ESPHomeYAMLError(u"Found multiple auto generate code endings, don't know" + u"which to chose, please remove one of them.") + + return text[:begin_index], text[(end_index + len(end_s)):] + + +def write_platformio_ini(content, path): + if os.path.isfile(path): + try: + with codecs.open(path, 'r', encoding='utf-8') as f: + text = f.read() + except OSError: + raise ESPHomeYAMLError(u"Could not read ini file at {}".format(path)) + prev_file = text + content_format = find_begin_end(text, INI_AUTO_GENERATE_BEGIN, INI_AUTO_GENERATE_END) + else: + prev_file = None + mkdir_p(os.path.dirname(path)) + content_format = INI_BASE_FORMAT + full_file = content_format[0] + INI_AUTO_GENERATE_BEGIN + '\n' + \ + content + INI_AUTO_GENERATE_END + content_format[1] + if prev_file == full_file: + return + with codecs.open(path, mode='w+', encoding='utf-8') as f: + f.write(full_file) + + +def write_cpp(code_s, path): + if os.path.isfile(path): + try: + with codecs.open(path, 'r', encoding='utf-8') as f: + text = f.read() + except OSError: + raise ESPHomeYAMLError(u"Could not read C++ file at {}".format(path)) + prev_file = text + code_format = find_begin_end(text, CPP_AUTO_GENERATE_BEGIN, CPP_AUTO_GENERATE_END) + else: + prev_file = None + mkdir_p(os.path.dirname(path)) + code_format = CPP_BASE_FORMAT + + full_file = code_format[0] + CPP_AUTO_GENERATE_BEGIN + '\n' + \ + code_s + CPP_AUTO_GENERATE_END + code_format[1] + if prev_file == full_file: + return + with codecs.open(path, 'w+', encoding='utf-8') as f: + f.write(full_file) diff --git a/esphomeyaml/yaml_util.py b/esphomeyaml/yaml_util.py new file mode 100644 index 0000000000..5acd53999f --- /dev/null +++ b/esphomeyaml/yaml_util.py @@ -0,0 +1,161 @@ +from __future__ import print_function +import codecs +import logging +from collections import OrderedDict + +import yaml + +from esphomeyaml.core import ESPHomeYAMLError, HexInt, IPAddress + +_LOGGER = logging.getLogger(__name__) + + +class NodeListClass(list): + """Wrapper class to be able to add attributes on a list.""" + + pass + + +class NodeStrClass(unicode): + """Wrapper class to be able to add attributes on a string.""" + + pass + + +class SafeLineLoader(yaml.SafeLoader): + """Loader class that keeps track of line numbers.""" + + def compose_node(self, parent, index): + """Annotate a node with the first line it was seen.""" + last_line = self.line # type: int + node = super(SafeLineLoader, self).compose_node(parent, index) # type: yaml.nodes.Node + node.__line__ = last_line + 1 + return node + + +def load_yaml(fname): + """Load a YAML file.""" + try: + with codecs.open(fname, encoding='utf-8') as conf_file: + return yaml.load(conf_file, Loader=SafeLineLoader) or OrderedDict() + except yaml.YAMLError as exc: + _LOGGER.error(exc) + raise ESPHomeYAMLError(exc) + except UnicodeDecodeError as exc: + _LOGGER.error(u"Unable to read file %s: %s", fname, exc) + raise ESPHomeYAMLError(exc) + + +def dump(dict_): + """Dump YAML to a string and remove null.""" + return yaml.safe_dump( + dict_, default_flow_style=False, allow_unicode=True) + + +def _ordered_dict(loader, node): + """Load YAML mappings into an ordered dictionary to preserve key order.""" + loader.flatten_mapping(node) + nodes = loader.construct_pairs(node) + + seen = {} + for (key, _), (child_node, _) in zip(nodes, node.value): + line = child_node.start_mark.line + + try: + hash(key) + except TypeError: + fname = getattr(loader.stream, 'name', '') + raise yaml.MarkedYAMLError( + context="invalid key: \"{}\"".format(key), + context_mark=yaml.Mark(fname, 0, line, -1, None, None) + ) + + if key in seen: + fname = getattr(loader.stream, 'name', '') + _LOGGER.error( + u'YAML file %s contains duplicate key "%s". ' + u'Check lines %d and %d.', fname, key, seen[key], line) + seen[key] = line + + return _add_reference(OrderedDict(nodes), loader, node) + + +def _construct_seq(loader, node): + """Add line number and file name to Load YAML sequence.""" + obj, = loader.construct_yaml_seq(node) + return _add_reference(obj, loader, node) + + +def _add_reference(obj, loader, node): + """Add file reference information to an object.""" + if isinstance(obj, (str, unicode)): + obj = NodeStrClass(obj) + if isinstance(obj, list): + return obj + setattr(obj, '__config_file__', loader.name) + setattr(obj, '__line__', node.start_mark.line) + return obj + + +yaml.SafeLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _ordered_dict) +yaml.SafeLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_SEQUENCE_TAG, _construct_seq) + + +# From: https://gist.github.com/miracle2k/3184458 +# pylint: disable=redefined-outer-name +def represent_odict(dump, tag, mapping, flow_style=None): + """Like BaseRepresenter.represent_mapping but does not issue the sort().""" + value = [] + node = yaml.MappingNode(tag, value, flow_style=flow_style) + if dump.alias_key is not None: + dump.represented_objects[dump.alias_key] = node + best_style = True + if hasattr(mapping, 'items'): + mapping = mapping.items() + for item_key, item_value in mapping: + node_key = dump.represent_data(item_key) + node_value = dump.represent_data(item_value) + if not (isinstance(node_key, yaml.ScalarNode) and not node_key.style): + best_style = False + if not (isinstance(node_value, yaml.ScalarNode) and + not node_value.style): + best_style = False + value.append((node_key, node_value)) + if flow_style is None: + if dump.default_flow_style is not None: + node.flow_style = dump.default_flow_style + else: + node.flow_style = best_style + return node + + +def unicode_representer(dumper, uni): + node = yaml.ScalarNode(tag=u'tag:yaml.org,2002:str', value=uni) + return node + + +def hex_int_representer(dumper, data): + node = yaml.ScalarNode(tag=u'tag:yaml.org,2002:int', value=str(data)) + return node + + +def ipaddress_representer(dumper, data): + node = yaml.ScalarNode(tag=u'tag:yaml.org,2002:str', value=str(data)) + return node + + +yaml.SafeDumper.add_representer( + OrderedDict, + lambda dumper, value: + represent_odict(dumper, 'tag:yaml.org,2002:map', value) +) + +yaml.SafeDumper.add_representer( + NodeListClass, + lambda dumper, value: + dumper.represent_sequence(dumper, 'tag:yaml.org,2002:map', value) +) + +yaml.SafeDumper.add_representer(unicode, unicode_representer) +yaml.SafeDumper.add_representer(HexInt, hex_int_representer) +yaml.SafeDumper.add_representer(IPAddress, ipaddress_representer) diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000000..92e1eb91da --- /dev/null +++ b/examples/README.md @@ -0,0 +1,3 @@ +# Examples + +This directory contains some of the ESP32/ESP8266 nodes I use at my home. diff --git a/examples/cabinet.yaml b/examples/cabinet.yaml new file mode 100644 index 0000000000..d5fba218b9 --- /dev/null +++ b/examples/cabinet.yaml @@ -0,0 +1,127 @@ +esphomeyaml: + name: cabinet + platform: ESP32 + board: nodemcu-32s + +logger: + level: verbose + +wifi: + ssid: '[SSID]' + password: '[PASSWORD]' + manual_ip: + static_ip: 192.168.178.203 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + +ota: + +mqtt: + broker: 192.168.178.84 + username: cabinet + password: '[PASSWORD]' + # This is the default + discovery: true + +power_supply: + - id: 'atx' + pin: + number: 13 + inverted: true + +i2c: + sda: 14 + scl: 27 + frequency: 400000 + +pca9685: + - id: 'pca9685' + frequency: 500 + +output: + - platform: pca9685 + pca9685_id: 'pca9685' + id: 'cabinet1_red' + channel: 14 + power_supply: 'atx' + - platform: pca9685 + pca9685_id: 'pca9685' + id: 'cabinet1_green' + channel: 15 + power_supply: 'atx' + - platform: pca9685 + pca9685_id: 'pca9685' + id: 'cabinet1_blue' + channel: 13 + power_supply: 'atx' + - platform: pca9685 + pca9685_id: 'pca9685' + id: 'cabinet2_red' + channel: 11 + power_supply: 'atx' + - platform: pca9685 + pca9685_id: 'pca9685' + id: 'cabinet2_green' + channel: 12 + power_supply: 'atx' + - platform: pca9685 + pca9685_id: 'pca9685' + id: 'cabinet2_blue' + channel: 10 + power_supply: 'atx' + - platform: pca9685 + pca9685_id: 'pca9685' + id: 'room_red' + channel: 8 + power_supply: 'atx' + - platform: pca9685 + pca9685_id: 'pca9685' + id: 'room_green' + channel: 9 + power_supply: 'atx' + - platform: pca9685 + pca9685_id: 'pca9685' + id: 'room_blue' + channel: 7 + power_supply: 'atx' + +light: + - platform: rgb + name: 'Cabinet Light 1' + red: 'cabinet1_red' + green: 'cabinet1_green' + blue: 'cabinet1_blue' + - platform: rgb + name: 'Cabinet Light 2' + red: 'cabinet2_red' + green: 'cabinet2_green' + blue: 'cabinet2_blue' + - platform: rgb + name: 'Room Light' + red: 'room_red' + green: 'room_green' + blue: 'room_blue' + +sensor: + - platform: dht + pin: 23 + temperature: + name: 'Cabinet Temperature' + humidity: + name: 'Cabinet Humidity' + model: DHT22 + +binary_sensor: + - platform: gpio + pin: 25 + name: 'Cabinet Motion' + device_class: motion + # Simple binary sensor that uses last will and birth messages to show + # node state + - platform: status + name: "Cabinet Status" + +switch: + # Simple switch that restarts the ESP32 + - platform: restart + name: "Cabinet Restart" diff --git a/examples/dachboden.yaml b/examples/dachboden.yaml new file mode 100644 index 0000000000..9406fc38a4 --- /dev/null +++ b/examples/dachboden.yaml @@ -0,0 +1,57 @@ +esphomeyaml: + name: dachboden + platform: ESP8266 + board: nodemcuv2 + +logger: + level: verbose + +wifi: + ssid: '[SSID]' + password: '[PASSWORD]' + manual_ip: + static_ip: 192.168.178.212 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + +ota: + +mqtt: + broker: 192.168.178.84 + username: dachboden + password: '[PASSWORD]' + # This is the default + discovery: true + + +dallas: + id: 'dallas' + pin: D1 + +sensor: + - platform: dht + pin: D3 + temperature: + name: 'Dachboden Temperatur' + humidity: + name: 'Dachboden Luftfeuchtigkeit' + model: DHT22 + - platform: dallas + dallas_id: 'dallas' + address: 0x01031663650aff28 + name: "Dachboden Solar Süd Vorlauf" + - platform: dallas + dallas_id: 'dallas' + address: 0x2b0416638fe6ff28 + name: "Dachboden Solar Süd Rücklauf" + - platform: adc + pin: A0 + name: "Dachboden Helligkeit" + +binary_sensor: + - platform: status + name: "Dachboden Status" + +switch: + - platform: restart + name: "Dachboden Neustart" diff --git a/examples/heatpump.yaml b/examples/heatpump.yaml new file mode 100644 index 0000000000..9ba902049c --- /dev/null +++ b/examples/heatpump.yaml @@ -0,0 +1,117 @@ +esphomeyaml: + name: heatpump + platform: ESP32 + board: nodemcu-32s + +logger: + level: verbose + +wifi: + ssid: '[SSID]' + password: '[PASSWORD]' + manual_ip: + static_ip: 192.168.178.204 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + +ota: + +mqtt: + broker: 192.168.178.84 + username: heatpump + password: '[PASSWORD]' + # This is the default + discovery: true + +dallas: + id: 'dallas' + pin: 15 + +sensor: + - platform: dht + pin: 0 + temperature: + name: 'Outside Temperature' + humidity: + name: 'Outside Humidity' + model: DHT22 + - platform: pulse_counter + pin: 12 + unit_of_measurement: 'kW' + name: 'Stromverbrauch Wintergarten' + update_interval: 30s + expire_after: 60s + filters: + - multiply: 0.06 + - platform: pulse_counter + pin: 13 + unit_of_measurement: 'kW' + name: 'Stromverbrauch Wärmepumpe' + update_interval: 30s + expire_after: 60s + filters: + - multiply: 0.06 + - platform: pulse_counter + pin: 14 + unit_of_measurement: 'kW' + name: 'Stromverbrauch Gesamt' + update_interval: 30s + expire_after: 60s + filters: + - multiply: 0.06 + - platform: dallas + dallas_id: 'dallas' + address: 0xfe0000031f1eaf28 + name: "Boiler Temperatur Oben" + - platform: dallas + dallas_id: 'dallas' + address: 0xba0000031f0e5228 + name: "Boiler Temperatur Unten" + - platform: dallas + dallas_id: 'dallas' + address: 0xa40000031f055028 + name: "Boiler Temperatur Mitte" + - platform: dallas + dallas_id: 'dallas' + address: 0x790000031ee1dc28 + name: "Heizung Rücklauf" + - platform: dallas + dallas_id: 'dallas' + address: 0xdd0000031efb0428 + name: "Ölheizung Vorlauf" + - platform: dallas + dallas_id: 'dallas' + address: 0x710000031f0e7e28 + name: "Boiler Solar Rücklauf" + - platform: dallas + dallas_id: 'dallas' + address: 0x92041703081aff28 + name: "Boiler Solar Vorlauf" + - platform: dallas + dallas_id: 'dallas' + address: 0x2c04173159f4ff28 + name: "Heizung Vorlauf" + - platform: dallas + dallas_id: 'dallas' + address: 0xd10417315babff28 + name: "Wärmepumpe Vorlauf" + - platform: dallas + dallas_id: 'dallas' + address: 0x6c0517024a17ff28 + name: "Boiler Heizung Vorlauf" + - platform: dallas + dallas_id: 'dallas' + address: 0x7d04173139eeff28 + name: "Wärmepumpe Rücklauf" + - platform: dallas + dallas_id: 'dallas' + address: 0x3204166398a5ff28 + name: "Wärmepumpe Verdampfer" + +binary_sensor: + - platform: status + name: "Heizung Status" + +switch: + - platform: restart + name: "Heizung Neustart" diff --git a/examples/kuche.yaml b/examples/kuche.yaml new file mode 100644 index 0000000000..7cb86f3fac --- /dev/null +++ b/examples/kuche.yaml @@ -0,0 +1,60 @@ +esphomeyaml: + name: kuche + platform: ESP8266 + board: nodemcuv2 + +logger: + level: verbose + +wifi: + ssid: '[SSID]' + password: '[PASSWORD]' + manual_ip: + static_ip: 192.168.178.211 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + +ota: + +mqtt: + broker: 192.168.178.84 + username: kuche + password: '[PASSWORD]' + # This is the default + discovery: true + + +dallas: + id: 'dallas' + pin: D1 + +sensor: + - platform: dallas + dallas_id: 'dallas' + address: 0x69041662d7f1ff28 + name: "Küche Raumtemperatur" + - platform: dallas + dallas_id: 'dallas' + address: 0x800416636bebff28 + name: "Küche Heizkörpertemperatur" + - platform: adc + pin: A0 + name: "Küche Helligkeit" + +output: + - platform: gpio + pin: D2 + id: 'ventilator' + +fan: + - platform: binary + output: 'ventilator' + name: 'Küche Heizkörper Ventilator' + +binary_sensor: + - platform: status + name: "Küche Status" + +switch: + - platform: restart + name: "Küche Neustart" diff --git a/examples/lebensmittelkeller.yaml b/examples/lebensmittelkeller.yaml new file mode 100644 index 0000000000..1c407807b5 --- /dev/null +++ b/examples/lebensmittelkeller.yaml @@ -0,0 +1,58 @@ +esphomeyaml: + name: lebensmittelkeller + platform: ESP8266 + board: nodemcuv2 + +logger: + level: verbose + +wifi: + ssid: '[SSID]' + password: '[PASSWORD]' + manual_ip: + static_ip: 192.168.178.209 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + +ota: + +mqtt: + broker: 192.168.178.84 + username: lebensmittelkeller + password: '[PASSWORD]' + # This is the default + discovery: true + +sensor: + - platform: dht + pin: D3 + temperature: + name: 'Lebensmittelkeller Temperatur' + humidity: + name: 'Lebensmittelkeller Feuchtigkeit' + model: DHT22 + - platform: adc + pin: A0 + name: "Lebensmittelkeller Helligkeit" + +output: + - platform: gpio + pin: D4 + id: 'ventilator' + +fan: + - platform: binary + output: 'ventilator' + name: 'Lebensmittelkeller Ventilator' + +switch: + - platform: gpio + pin: D2 + name: 'Lebensmittelkeller Entfeuchter' + icon: 'mdi:water-off' + - platform: restart + name: "Lebensmittelkeller Neustart" + +binary_sensor: + - platform: status + name: "Lebensmittelkeller Status" diff --git a/examples/livingroom.yaml b/examples/livingroom.yaml new file mode 100644 index 0000000000..c0bd577096 --- /dev/null +++ b/examples/livingroom.yaml @@ -0,0 +1,344 @@ +esphomeyaml: + name: livingroom + platform: ESP32 + board: nodemcu-32s + +logger: + level: verbose + +wifi: + ssid: '[SSID]' + password: '[PASSWORD]' + manual_ip: + static_ip: 192.168.178.201 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + +ota: + +mqtt: + broker: 192.168.178.84 + username: livingroom + password: '[PASSWORD]' + # This is the default + discovery: true + +output: + - platform: ledc + id: 'fan_float' + frequency: 50000 + pin: 22 + bit_depth: 8 + +dallas: + pin: 23 + id: dallas + +sensor: + - platform: dallas + dallas_id: dallas + address: 0x1c0000031edd2a28 + name: "Wohnzimmer Raumtemperatur" + filters: + - sliding_window_moving_average: + window_size: 15 + send_every: 15 + - filter_out: 85 + - platform: dallas + dallas_id: dallas + address: 0x7a0315a8371eff28 + name: "Wohnzimmer Heizkörpertemperatur" + update_interval: 30s + filters: + - sliding_window_moving_average: + window_size: 15 + send_every: 15 + - filter_out: 85 + +fan: + - platform: speed + output: 'fan_float' + name: 'Wohnzimmer Heizkörper Ventilator' + +binary_sensor: + - platform: status + name: "Wohnzimmer Status" + +ir_transmitter: + pin: 32 + id: 'ir' + +switch: + - platform: restart + name: "Wohnzimmer Neustart" + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV On" + panasonic: + address: 0x4004 + command: 0x100BCBD + repeat: 25 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Off" + panasonic: + address: 0x4004 + command: 0x100BCBD + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV SD Card" + panasonic: + address: 0x4004 + command: 0x190D544 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Input TV" + panasonic: + address: 0x4004 + command: 0x1400C4D + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Input AV" + panasonic: + address: 0x4004 + command: 0x100A0A1 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Menu" + panasonic: + address: 0x4004 + command: 0x1004A4B + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Aspect Ratio" + panasonic: + address: 0x4004 + command: 0x1207B5A + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Viera Cast" + panasonic: + address: 0x4004 + command: 0x190C958 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Direct TV REC" + panasonic: + address: 0x4004 + command: 0x1909100 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Info" + panasonic: + address: 0x4004 + command: 0x1009C9D + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Exit" + panasonic: + address: 0x4004 + command: 0x100CBCA + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Viera Link" + panasonic: + address: 0x4004 + command: 0x1908D1C + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Viera Tools" + panasonic: + address: 0x4004 + command: 0x100F7F6 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Guide" + panasonic: + address: 0x4004 + command: 0x190E170 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Up" + panasonic: + address: 0x4004 + command: 0x1005253 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Left" + panasonic: + address: 0x4004 + command: 0x1007273 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV OK" + panasonic: + address: 0x4004 + command: 0x1009293 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Right" + panasonic: + address: 0x4004 + command: 0x100F2F3 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Down" + panasonic: + address: 0x4004 + command: 0x100D2D3 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Option" + panasonic: + address: 0x4004 + command: 0x190E574 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Back" + panasonic: + address: 0x4004 + command: 0x1002B2A + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Red" + panasonic: + address: 0x4004 + command: 0x1000E0F + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Green" + panasonic: + address: 0x4004 + command: 0x1000E0F # TODO: FIXME + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Yellow" + panasonic: + address: 0x4004 + command: 0x1004E4F + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Blue" + panasonic: + address: 0x4004 + command: 0x100CECF + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Text" + panasonic: + address: 0x4004 + command: 0x180C041 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Subtitle" + panasonic: + address: 0x4004 + command: 0x180A021 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Index" + panasonic: + address: 0x4004 + command: 0x1801091 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Hold" + panasonic: + address: 0x4004 + command: 0x1809011 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV 1" + panasonic: + address: 0x4004 + command: 0x1000809 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV 2" + panasonic: + address: 0x4004 + command: 0x1008889 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV 3" + panasonic: + address: 0x4004 + command: 0x1004849 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV 4" + panasonic: + address: 0x4004 + command: 0x100C8C9 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV 5" + panasonic: + address: 0x4004 + command: 0x1002829 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV 6" + panasonic: + address: 0x4004 + command: 0x100A8A9 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV 7" + panasonic: + address: 0x4004 + command: 0x1006869 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV 8" + panasonic: + address: 0x4004 + command: 0x100E8E9 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV 9" + panasonic: + address: 0x4004 + command: 0x1001819 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV 0" + panasonic: + address: 0x4004 + command: 0x1009899 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Mute" + panasonic: + address: 0x4004 + command: 0x1004C4D + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Last View" + panasonic: + address: 0x4004 + command: 0x100ECED + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Volume Up" + panasonic: + address: 0x4004 + command: 0x1000405 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Volume Down" + panasonic: + address: 0x4004 + command: 0x1008485 + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Program Up" + panasonic: + address: 0x4004 + command: 0x1002C2D + - platform: ir_transmitter + ir_transmitter_id: 'ir' + name: "Panasonic TV Program Down" + panasonic: + address: 0x4004 + command: 0x100ACAD diff --git a/examples/terrasse.yaml b/examples/terrasse.yaml new file mode 100644 index 0000000000..6c1481ebca --- /dev/null +++ b/examples/terrasse.yaml @@ -0,0 +1,48 @@ +esphomeyaml: + name: terrasse + platform: ESP32 + board: nodemcu-32s + +logger: + level: verbose + +wifi: + ssid: '[SSID]' + password: '[PASSWORD]' + manual_ip: + static_ip: 192.168.178.205 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + +ota: + +mqtt: + broker: 192.168.178.84 + username: terrasse + password: '[PASSWORD]' + # This is the default + discovery: true + +dallas: + pin: 25 + id: dallas + +sensor: + - platform: pulse_counter + pin: 34 + name: "Terrasse Wind" + - platform: pulse_counter + pin: 39 + name: "Terrasse Regen" + - platform: dallas + dallas_id: dallas + index: 0 + name: "Terrasse Temperatur" + +binary_sensor: + - platform: status + name: "Terrasse Status" + +switch: + - platform: restart + name: "Terrasse Neustart" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..1cec484c87 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +voluptuous==0.11.1 +platformio==3.5.2 +pyyaml==3.12 +paho-mqtt==1.3.1 +colorlog==3.1.2 diff --git a/setup.py b/setup.py new file mode 100755 index 0000000000..860d201321 --- /dev/null +++ b/setup.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +"""esphomeyaml setup script.""" +from setuptools import setup + +from esphomeyaml import const + +PROJECT_NAME = 'esphomeyaml' +PROJECT_PACKAGE_NAME = 'esphomeyaml' +PROJECT_LICENSE = 'MIT' +PROJECT_AUTHOR = 'Otto Winter' +PROJECT_COPYRIGHT = '2018, Otto Winter' +PROJECT_URL = 'http://esphomelib.com/' +PROJECT_EMAIL = 'contact@otto-winter.com' + +PROJECT_GITHUB_USERNAME = 'OttoWinter' +PROJECT_GITHUB_REPOSITORY = 'esphomelib' + +PYPI_URL = 'https://pypi.python.org/pypi/{}'.format(PROJECT_PACKAGE_NAME) +GITHUB_PATH = '{}/{}'.format(PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY) +GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH) + +DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, const.__version__) + +REQUIRES = [ + 'voluptuous>=0.11.1', + 'platformio>=3.5.2', + 'pyyaml>=3.12', + 'paho-mqtt>=1.3.1', + 'colorlog>=3.1.2', +] + +CLASSIFIERS = [ + 'Environment :: Console', + 'Intended Audience :: Developers', + 'Intended Audience :: End Users/Desktop', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: C++', + 'Programming Language :: Python :: 2 :: Only', + 'Topic :: Home Automation', +] + +setup( + name=PROJECT_PACKAGE_NAME, + version=const.__version__, + license=PROJECT_LICENSE, + url=GITHUB_URL, + download_url=DOWNLOAD_URL, + author=PROJECT_AUTHOR, + author_email=PROJECT_EMAIL, + description="Make creating custom firmwares for ESP32/ESP8266 super easy.", + include_package_data=True, + zip_safe=False, + platforms='any', + test_suite='tests', + python_requires='>=2.7,<3', + install_requires=REQUIRES, + keywords=['home', 'automation'], + entry_points={ + 'console_scripts': [ + 'esphomeyaml = esphomeyaml.__main__:main' + ] + } +)