Add native ESPHome API (#265)

* Esphomeapi

* Updates

* Remove MQTT from wizard

* Add protobuf to requirements

* Fix

* API Client updates

* Dump config on API connect

* Old WiFi config migration

* Home Assistant state import

* Lint
This commit is contained in:
Otto Winter 2018-12-18 19:31:43 +01:00 committed by GitHub
parent 7556845079
commit da2821ab36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 3783 additions and 346 deletions

View File

@ -1,6 +1,6 @@
--- ---
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help esphomelib improve
--- ---
@ -9,7 +9,9 @@ about: Create a report to help us improve
- esphomeyaml [here] - This is mostly for reporting bugs when compiling and when you get a long stack trace while compiling or if a configuration fails to validate. - esphomeyaml [here] - This is mostly for reporting bugs when compiling and when you get a long stack trace while compiling or if a configuration fails to validate.
- esphomelib [https://github.com/OttoWinter/esphomelib] - Report bugs there if the ESP is crashing or a feature is not working as expected. - esphomelib [https://github.com/OttoWinter/esphomelib] - Report bugs there if the ESP is crashing or a feature is not working as expected.
- esphomedocs [https://github.com/OttoWinter/esphomedocs] - Report bugs there if the documentation is wrong/outdated. - esphomedocs [https://github.com/OttoWinter/esphomedocs] - Report bugs there if the documentation is wrong/outdated.
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks (```). Do not delete any text from this template! - Provide as many details as possible. Paste logs, configuration sample and code into the backticks (```).
DO NOT DELETE ANY TEXT from this template! Otherwise the issue may be closed without a comment.
--> -->
**Operating environment (Hass.io/Docker/pip/etc.):** **Operating environment (Hass.io/Docker/pip/etc.):**
@ -33,7 +35,7 @@ Please add the link to the documentation at https://esphomelib.com/esphomeyaml/i
**Problem-relevant YAML-configuration entries:** **Problem-relevant YAML-configuration entries:**
```yaml ```yaml
PASTE YAML FILE HERE
``` ```
**Traceback (if applicable):** **Traceback (if applicable):**

View File

@ -7,16 +7,15 @@ about: Suggest an idea for this project
<!-- READ THIS FIRST: <!-- READ THIS FIRST:
- This is for feature requests only, if you want to have a certain new sensor/module supported, please use the "new integration" template. - This is for feature requests only, if you want to have a certain new sensor/module supported, please use the "new integration" template.
- Please be as descriptive as possible, especially use-cases that can otherwise not be solved boost the problem's priority. - Please be as descriptive as possible, especially use-cases that can otherwise not be solved boost the problem's priority.
DO NOT DELETE ANY TEXT from this template! Otherwise the issue may be closed without a comment.
--> -->
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem/use-case? Please describe.**
<!-- <!-- A clear and concise description of what the problem is. -->
A clear and concise description of what the problem is.
-->
Ex. I'm always frustrated when [...]
**Describe the solution you'd like** **Describe the solution you'd like:**
A description of what you want to happen. <!-- A description of what you want to happen. -->
**Additional context** **Additional context:**
Add any other context about the feature request here. <!-- Add any other context about the feature request here. -->

View File

@ -4,17 +4,10 @@ about: Suggest a new integration for esphomelib
--- ---
<!-- READ THIS FIRST: DO NOT POST NEW INTEGRATION REQUESTS HERE!
- This is for new integrations (such as new sensors/modules) only, for new features within the environment please use the "feature request" template.
- Do not delete anything from this template and fill out the form as precisely as possible.
-->
**What new integration would you wish to have?** Please post all new integration requests in the esphomelib repository:
<!-- A name/description of the new integration/board. -->
**If possible, provide a link to an existing library for the integration:** https://github.com/OttoWinter/esphomelib/issues
**Is your feature request related to a problem? Please describe.** Thank you!
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Additional context**

View File

@ -6,15 +6,9 @@
**Pull request in [esphomedocs](https://github.com/OttoWinter/esphomedocs) with documentation (if applicable):** OttoWinter/esphomedocs#<esphomedocs PR number goes here> **Pull request in [esphomedocs](https://github.com/OttoWinter/esphomedocs) with documentation (if applicable):** OttoWinter/esphomedocs#<esphomedocs PR number goes here>
**Pull request in [esphomelib](https://github.com/OttoWinter/esphomelib) with C++ framework changes (if applicable):** OttoWinter/esphomelib#<esphomelib PR number goes here> **Pull request in [esphomelib](https://github.com/OttoWinter/esphomelib) with C++ framework changes (if applicable):** OttoWinter/esphomelib#<esphomelib PR number goes here>
## Example entry for YAML configuration (if applicable):
```yaml
```
## Checklist: ## Checklist:
- [ ] The code change is tested and works locally. - [ ] The code change is tested and works locally.
- [ ] Tests have been added to verify that the new code works (under `tests/` folder). - [ ] Tests have been added to verify that the new code works (under `tests/` folder).
- [ ] Check this box if you have read, understand, comply, and agree with the [Code of Conduct](https://github.com/OttoWinter/esphomeyaml/blob/master/CODE_OF_CONDUCT.md).
If user exposed functionality or configuration variables are added/changed: If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [esphomedocs](https://github.com/OttoWinter/esphomedocs). - [ ] Documentation added/updated in [esphomedocs](https://github.com/OttoWinter/esphomedocs).

View File

@ -14,12 +14,9 @@
], ],
"hassio_api": true, "hassio_api": true,
"auth_api": true, "auth_api": true,
"services": [
"mqtt:want"
],
"hassio_role": "default", "hassio_role": "default",
"homeassistant_api": false, "homeassistant_api": false,
"host_network": false, "host_network": true,
"boot": "auto", "boot": "auto",
"ports": { "ports": {
"6052/tcp": 6052 "6052/tcp": 6052

View File

@ -9,6 +9,7 @@ import random
import sys import sys
from esphomeyaml import const, core_config, mqtt, platformio_api, wizard, writer, yaml_util from esphomeyaml import const, core_config, mqtt, platformio_api, wizard, writer, yaml_util
from esphomeyaml.api.client import run_logs
from esphomeyaml.config import get_component, iter_components, read_config, strip_default_ids from esphomeyaml.config import get_component, iter_components, read_config, strip_default_ids
from esphomeyaml.const import CONF_BAUD_RATE, CONF_DOMAIN, CONF_ESPHOMEYAML, \ from esphomeyaml.const import CONF_BAUD_RATE, CONF_DOMAIN, CONF_ESPHOMEYAML, \
CONF_HOSTNAME, CONF_LOGGER, CONF_MANUAL_IP, CONF_NAME, CONF_STATIC_IP, CONF_USE_CUSTOM_CODE, \ CONF_HOSTNAME, CONF_LOGGER, CONF_MANUAL_IP, CONF_NAME, CONF_STATIC_IP, CONF_USE_CUSTOM_CODE, \
@ -22,7 +23,7 @@ from esphomeyaml.util import run_external_command, safe_print
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PRE_INITIALIZE = ['esphomeyaml', 'logger', 'wifi', 'ota', 'mqtt', 'web_server', 'i2c'] PRE_INITIALIZE = ['esphomeyaml', 'logger', 'wifi', 'ota', 'mqtt', 'web_server', 'api', 'i2c']
def get_serial_ports(): def get_serial_ports():
@ -202,6 +203,8 @@ def show_logs(config, args, port):
if port != 'OTA' and serial_port: if port != 'OTA' and serial_port:
run_miniterm(config, port) run_miniterm(config, port)
return 0 return 0
if 'api' in config:
return run_logs(config, get_upload_host(config))
return mqtt.show_logs(config, args.topic, args.username, args.password, args.client_id) return mqtt.show_logs(config, args.topic, args.username, args.password, args.client_id)
@ -368,6 +371,8 @@ def parse_args(argv):
parser = argparse.ArgumentParser(prog='esphomeyaml') parser = argparse.ArgumentParser(prog='esphomeyaml')
parser.add_argument('-v', '--verbose', help="Enable verbose esphomeyaml logs.", parser.add_argument('-v', '--verbose', help="Enable verbose esphomeyaml logs.",
action='store_true') action='store_true')
parser.add_argument('--dashboard', help="Internal flag to set if the command is run from the "
"dashboard.", action='store_true')
parser.add_argument('configuration', help='Your YAML configuration file.') parser.add_argument('configuration', help='Your YAML configuration file.')
subparsers = parser.add_subparsers(help='Commands', dest='command') subparsers = parser.add_subparsers(help='Commands', dest='command')
@ -445,6 +450,7 @@ def parse_args(argv):
def run_esphomeyaml(argv): def run_esphomeyaml(argv):
args = parse_args(argv) args = parse_args(argv)
CORE.dashboard = args.dashboard
setup_log(args.verbose) setup_log(args.verbose)
if args.command in PRE_CONFIG_ACTIONS: if args.command in PRE_CONFIG_ACTIONS:

View File

329
esphomeyaml/api/api.proto Normal file
View File

@ -0,0 +1,329 @@
syntax = "proto3";
// The Home Assistant protocol is structured as a simple
// TCP socket with short binary messages encoded in the protocol buffers format
// First, a message in this protocol has a specific format:
// * VarInt denoting the size of the message object. (type is not part of this)
// * VarInt denoting the type of message.
// * The message object encoded as a ProtoBuf message
// The connection is established in 4 steps:
// * First, the client connects to the server and sends a "Hello Request" identifying itself
// * The server responds with a "Hello Response" and selects the protocol version
// * After receiving this message, the client attempts to authenticate itself using
// the password and a "Connect Request"
// * The server responds with a "Connect Response" and notifies of invalid password.
// If anything in this initial process fails, the connection must immediately closed
// by both sides and _no_ disconnection message is to be sent.
// Message sent at the beginning of each connection
// Can only be sent by the client and only at the beginning of the connection
message HelloRequest {
// Description of client (like User Agent)
// For example "Home Assistant"
// Not strictly necessary to send but nice for debugging
// purposes.
string client_info = 1;
}
// Confirmation of successful connection request.
// Can only be sent by the server and only at the beginning of the connection
message HelloResponse {
// The version of the API to use. The _client_ (for example Home Assistant) needs to check
// for compatibility and if necessary adopt to an older API.
// Major is for breaking changes in the base protocol - a mismatch will lead to immediate disconnect_client_
// Minor is for breaking changes in individual messages - a mismatch will lead to a warning message
uint32 api_version_major = 1;
uint32 api_version_minor = 2;
// A string identifying the server (ESP); like client info this may be empty
// and only exists for debugging/logging purposes.
// For example "ESPHome v1.10.0 on ESP8266"
string server_info = 3;
}
// Message sent at the beginning of each connection to authenticate the client
// Can only be sent by the client and only at the beginning of the connection
message ConnectRequest {
// The password to log in with
string password = 1;
}
// Confirmation of successful connection. After this the connection is available for all traffic.
// Can only be sent by the server and only at the beginning of the connection
message ConnectResponse {
bool invalid_password = 1;
}
// Request to close the connection.
// Can be sent by both the client and server
message DisconnectRequest {
// Do not close the connection before the acknowledgement arrives
}
message DisconnectResponse {
// Empty - Both parties are required to close the connection after this
// message has been received.
}
message PingRequest {
// Empty
}
message PingResponse {
// Empty
}
message DeviceInfoRequest {
// Empty
}
message DeviceInfoResponse {
bool uses_password = 1;
// The name of the node, given by "App.set_name()"
string name = 2;
// The mac address of the device. For example "AC:BC:32:89:0E:A9"
string mac_address = 3;
// A string describing the ESPHome version. For example "1.10.0"
string esphome_core_version = 4;
// A string describing the date of compilation, this is generated by the compiler
// and therefore may not be in the same format all the time.
// If the user isn't using esphomeyaml, this will also not be set.
string compilation_time = 5;
// The model of the board. For example NodeMCU
string model = 6;
bool has_deep_sleep = 7;
}
message ListEntitiesRequest {
// Empty
}
message ListEntitiesBinarySensorResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string device_class = 5;
bool is_status_binary_sensor = 6;
}
message ListEntitiesCoverResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
bool is_optimistic = 5;
}
message ListEntitiesFanResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
bool supports_oscillation = 5;
bool supports_speed = 6;
}
message ListEntitiesLightResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
bool supports_brightness = 5;
bool supports_rgb = 6;
bool supports_white_value = 7;
bool supports_color_temperature = 8;
float min_mireds = 9;
float max_mireds = 10;
repeated string effects = 11;
}
message ListEntitiesSensorResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
string unit_of_measurement = 6;
int32 accuracy_decimals = 7;
}
message ListEntitiesSwitchResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
bool optimistic = 6;
}
message ListEntitiesTextSensorResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
}
message ListEntitiesDoneResponse {
// Empty
}
message SubscribeStatesRequest {
// Empty
}
message BinarySensorStateResponse {
fixed32 key = 1;
bool state = 2;
}
message CoverStateResponse {
fixed32 key = 1;
enum CoverState {
OPEN = 0;
CLOSED = 1;
}
CoverState state = 2;
}
enum FanSpeed {
LOW = 0;
MEDIUM = 1;
HIGH = 2;
}
message FanStateResponse {
fixed32 key = 1;
bool state = 2;
bool oscillating = 3;
FanSpeed speed = 4;
}
message LightStateResponse {
fixed32 key = 1;
bool state = 2;
float brightness = 3;
float red = 4;
float green = 5;
float blue = 6;
float white = 7;
float color_temperature = 8;
string effect = 9;
}
message SensorStateResponse {
fixed32 key = 1;
float state = 2;
}
message SwitchStateResponse {
fixed32 key = 1;
bool state = 2;
}
message TextSensorStateResponse {
fixed32 key = 1;
string state = 2;
}
message CoverCommandRequest {
fixed32 key = 1;
enum CoverCommand {
OPEN = 0;
CLOSE = 1;
STOP = 2;
}
bool has_state = 2;
CoverCommand command = 3;
}
message FanCommandRequest {
fixed32 key = 1;
bool has_state = 2;
bool state = 3;
bool has_speed = 4;
FanSpeed speed = 5;
bool has_oscillating = 6;
bool oscillating = 7;
}
message LightCommandRequest {
fixed32 key = 1;
bool has_state = 2;
bool state = 3;
bool has_brightness = 4;
float brightness = 5;
bool has_rgb = 6;
float red = 7;
float green = 8;
float blue = 9;
bool has_white = 10;
float white = 11;
bool has_color_temperature = 12;
float color_temperature = 13;
bool has_transition_length = 14;
uint32 transition_length = 15;
bool has_flash_length = 16;
uint32 flash_length = 17;
bool has_effect = 18;
string effect = 19;
}
message SwitchCommandRequest {
fixed32 key = 1;
bool state = 2;
}
enum LogLevel {
NONE = 0;
ERROR = 1;
WARN = 2;
INFO = 3;
DEBUG = 4;
VERBOSE = 5;
VERY_VERBOSE = 6;
}
message SubscribeLogsRequest {
LogLevel level = 1;
bool dump_config = 2;
}
message SubscribeLogsResponse {
LogLevel level = 1;
string tag = 2;
string message = 3;
}
message SubscribeServiceCallsRequest {
}
message ServiceCallResponse {
string service = 1;
map<string, string> data = 2;
map<string, string> data_template = 3;
map<string, string> variables = 4;
}
// 1. Client sends SubscribeHomeAssistantStatesRequest
// 2. Server responds with zero or more SubscribeHomeAssistantStateResponse (async)
// 3. Client sends HomeAssistantStateResponse for state changes.
message SubscribeHomeAssistantStatesRequest {
}
message SubscribeHomeAssistantStateResponse {
string entity_id = 1;
}
message HomeAssistantStateResponse {
string entity_id = 1;
string state = 2;
}
message GetTimeRequest {
}
message GetTimeResponse {
fixed32 epoch_seconds = 1;
}

2445
esphomeyaml/api/api_pb2.py Normal file

File diff suppressed because one or more lines are too long

474
esphomeyaml/api/client.py Normal file
View File

@ -0,0 +1,474 @@
from datetime import datetime
import functools
import logging
import socket
import threading
import time
# pylint: disable=unused-import
from typing import Optional # noqa
from google.protobuf import message
from esphomeyaml import const
import esphomeyaml.api.api_pb2 as pb
from esphomeyaml.const import CONF_PASSWORD, CONF_PORT
from esphomeyaml.core import EsphomeyamlError
from esphomeyaml.helpers import resolve_ip_address
from esphomeyaml.util import safe_print
_LOGGER = logging.getLogger(__name__)
class APIConnectionError(EsphomeyamlError):
pass
MESSAGE_TYPE_TO_PROTO = {
1: pb.HelloRequest,
2: pb.HelloResponse,
3: pb.ConnectRequest,
4: pb.ConnectResponse,
5: pb.DisconnectRequest,
6: pb.DisconnectResponse,
7: pb.PingRequest,
8: pb.PingResponse,
9: pb.DeviceInfoRequest,
10: pb.DeviceInfoResponse,
11: pb.ListEntitiesRequest,
12: pb.ListEntitiesBinarySensorResponse,
13: pb.ListEntitiesCoverResponse,
14: pb.ListEntitiesFanResponse,
15: pb.ListEntitiesLightResponse,
16: pb.ListEntitiesSensorResponse,
17: pb.ListEntitiesSwitchResponse,
18: pb.ListEntitiesTextSensorResponse,
19: pb.ListEntitiesDoneResponse,
20: pb.SubscribeStatesRequest,
21: pb.BinarySensorStateResponse,
22: pb.CoverStateResponse,
23: pb.FanStateResponse,
24: pb.LightStateResponse,
25: pb.SensorStateResponse,
26: pb.SwitchStateResponse,
27: pb.TextSensorStateResponse,
28: pb.SubscribeLogsRequest,
29: pb.SubscribeLogsResponse,
30: pb.CoverCommandRequest,
31: pb.FanCommandRequest,
32: pb.LightCommandRequest,
33: pb.SwitchCommandRequest,
34: pb.SubscribeServiceCallsRequest,
35: pb.ServiceCallResponse,
36: pb.GetTimeRequest,
37: pb.GetTimeResponse,
}
def _varuint_to_bytes(value):
if value <= 0x7F:
return chr(value)
ret = bytes()
while value:
temp = value & 0x7F
value >>= 7
if value:
ret += chr(temp | 0x80)
else:
ret += chr(temp)
return ret
def _bytes_to_varuint(value):
result = 0
bitpos = 0
for c in value:
val = ord(c)
result |= (val & 0x7F) << bitpos
bitpos += 7
if (val & 0x80) == 0:
return result
return None
# pylint: disable=too-many-instance-attributes,not-callable
class APIClient(threading.Thread):
def __init__(self, address, port, password):
threading.Thread.__init__(self)
self._address = address # type: str
self._port = port # type: int
self._password = password # type: Optional[str]
self._socket = None # type: Optional[socket.socket]
self._connected = False
self._authenticated = False
self._message_handlers = []
self._keepalive = 5
self._ping_timer = None
self._refresh_ping()
self.on_disconnect = None
self.on_connect = None
self.on_login = None
self.auto_reconnect = False
self._running = False
self._stop_event = threading.Event()
self._socket_open = False
@property
def stopped(self):
return self._stop_event.is_set()
def _refresh_ping(self):
if self._ping_timer is not None:
self._ping_timer.cancel()
self._ping_timer = None
def func():
self._ping_timer = None
if self._connected:
try:
self.ping()
except APIConnectionError:
self._on_error()
self._refresh_ping()
self._ping_timer = threading.Timer(self._keepalive, func)
self._ping_timer.start()
def stop(self, force=False):
if self.stopped:
raise ValueError
if self._connected and not force:
try:
self.disconnect()
except APIConnectionError:
pass
if self._socket is not None:
self._socket.close()
self._socket = None
self._stop_event.set()
if self._ping_timer is not None:
self._ping_timer.cancel()
self._ping_timer = None
if not force:
self.join()
def connect(self):
if not self._running:
raise APIConnectionError("You need to call start() first!")
if self._connected:
raise APIConnectionError("Already connected!")
self._message_handlers = []
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._socket.settimeout(10.0)
self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
try:
ip = resolve_ip_address(self._address)
except EsphomeyamlError as err:
_LOGGER.warning("Error resolving IP address of %s. Is it connected to WiFi?",
self._address)
_LOGGER.warning("(If this error persists, please set a static IP address: "
"https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips)")
raise APIConnectionError(err)
_LOGGER.info("Connecting to %s:%s (%s)", self._address, self._port, ip)
try:
self._socket.connect((ip, self._port))
except socket.error as err:
self._on_error()
raise APIConnectionError("Error connecting to {}: {}".format(ip, err))
self._socket_open = True
self._socket.settimeout(0.1)
hello = pb.HelloRequest()
hello.client_info = 'esphomeyaml v{}'.format(const.__version__)
try:
resp = self._send_message_await_response(hello, pb.HelloResponse)
except APIConnectionError as err:
self._on_error()
raise err
_LOGGER.debug("Successfully connected to %s ('%s' API=%s.%s)", self._address,
resp.server_info, resp.api_version_major, resp.api_version_minor)
self._connected = True
if self.on_connect is not None:
self.on_connect()
def _check_connected(self):
if not self._connected:
self._on_error()
raise APIConnectionError("Must be connected!")
def login(self):
self._check_connected()
if self._authenticated:
raise APIConnectionError("Already logged in!")
connect = pb.ConnectRequest()
if self._password is not None:
connect.password = self._password
resp = self._send_message_await_response(connect, pb.ConnectResponse)
if resp.invalid_password:
raise APIConnectionError("Invalid password!")
self._authenticated = True
if self.on_login is not None:
self.on_login()
def _on_error(self):
if self._connected and self.on_disconnect is not None:
self.on_disconnect()
if self._socket is not None:
self._socket.close()
self._socket = None
self._socket_open = False
self._connected = False
self._authenticated = False
def _write(self, data): # type: (bytes) -> None
_LOGGER.debug("Write: %s", ' '.join('{:02X}'.format(ord(x)) for x in data))
try:
self._socket.sendall(data)
except socket.error as err:
self._on_error()
raise APIConnectionError("Error while writing data: {}".format(err))
def _send_message(self, msg):
# type: (message.Message) -> None
for message_type, klass in MESSAGE_TYPE_TO_PROTO.iteritems():
if isinstance(msg, klass):
break
else:
raise ValueError
encoded = msg.SerializeToString()
_LOGGER.debug("Sending %s: %s", type(message), unicode(message))
req = chr(0x00)
req += _varuint_to_bytes(len(encoded))
req += _varuint_to_bytes(message_type)
req += encoded
self._write(req)
self._refresh_ping()
def _send_message_await_response_complex(self, send_msg, do_append, do_stop, timeout=1):
event = threading.Event()
responses = []
def on_message(resp):
if do_append(resp):
responses.append(resp)
if do_stop(resp):
event.set()
self._message_handlers.append(on_message)
self._send_message(send_msg)
ret = event.wait(timeout)
try:
self._message_handlers.remove(on_message)
except ValueError:
pass
if not ret:
raise APIConnectionError("Timeout while waiting for message response!")
return responses
def _send_message_await_response(self, send_msg, response_type, timeout=1):
def is_response(msg):
return isinstance(msg, response_type)
return self._send_message_await_response_complex(send_msg, is_response, is_response,
timeout)[0]
def device_info(self):
self._check_connected()
return self._send_message_await_response(pb.DeviceInfoRequest(), pb.DeviceInfoResponse)
def ping(self):
self._check_connected()
return self._send_message_await_response(pb.PingRequest(), pb.PingResponse)
def disconnect(self):
self._check_connected()
try:
self._send_message_await_response(pb.DisconnectRequest(), pb.DisconnectResponse)
except APIConnectionError:
pass
if self._socket is not None:
self._socket.close()
self._socket = None
self._socket_open = False
self._connected = False
if self.on_disconnect is not None:
self.on_disconnect()
def _check_authenticated(self):
if not self._authenticated:
raise APIConnectionError("Must login first!")
def subscribe_logs(self, on_log, log_level=None, dump_config=False):
self._check_authenticated()
def on_msg(msg):
if isinstance(msg, pb.SubscribeLogsResponse):
on_log(msg)
self._message_handlers.append(on_msg)
req = pb.SubscribeLogsRequest(dump_config=dump_config)
if log_level is not None:
req.level = log_level
self._send_message(req)
def _recv(self, amount):
ret = bytes()
if amount == 0:
return ret
while len(ret) < amount:
if self.stopped:
raise APIConnectionError("Stopped!")
if self._socket is None or not self._socket_open:
raise APIConnectionError("No socket!")
try:
val = self._socket.recv(amount - len(ret))
except socket.timeout:
continue
except socket.error as err:
raise APIConnectionError("Error while receiving data: {}".format(err))
ret += val
return ret
def _recv_varint(self):
raw = bytes()
while not raw or ord(raw[-1]) & 0x80:
raw += self._recv(1)
return _bytes_to_varuint(raw)
def _run_once(self):
if self._socket is None or not self._socket_open:
time.sleep(0.1)
return
# Preamble
if ord(self._recv(1)[0]) != 0x00:
raise APIConnectionError("Invalid preamble")
length = self._recv_varint()
msg_type = self._recv_varint()
raw_msg = self._recv(length)
if msg_type not in MESSAGE_TYPE_TO_PROTO:
_LOGGER.debug("Skipping message type %s", msg_type)
return
msg = MESSAGE_TYPE_TO_PROTO[msg_type]()
msg.ParseFromString(raw_msg)
_LOGGER.debug("Got message of type %s: %s", type(msg), msg)
for msg_handler in self._message_handlers[:]:
msg_handler(msg)
self._handle_internal_messages(msg)
self._refresh_ping()
def run(self):
self._running = True
while not self.stopped:
try:
self._run_once()
except APIConnectionError as err:
if self.stopped:
break
if self._connected:
_LOGGER.error("Error while reading incoming messages: %s", err)
self._on_error()
self._running = False
def _handle_internal_messages(self, msg):
if isinstance(msg, pb.DisconnectRequest):
self._send_message(pb.DisconnectResponse())
if self._socket is not None:
self._socket.close()
self._socket = None
self._connected = False
self._socket_open = False
if self.on_disconnect is not None:
self.on_disconnect()
elif isinstance(msg, pb.PingRequest):
self._send_message(pb.PingResponse())
elif isinstance(msg, pb.GetTimeRequest):
resp = pb.GetTimeResponse()
resp.epoch_seconds = int(time.time())
self._send_message(resp)
def run_logs(config, address):
conf = config['api']
port = conf[CONF_PORT]
password = conf[CONF_PASSWORD]
_LOGGER.info("Starting log output from %s using esphomelib API", address)
cli = APIClient(address, port, password)
stopping = False
retry_timer = []
def try_connect(tries=0, is_disconnect=True):
if stopping:
return
if is_disconnect:
_LOGGER.warning(u"Disconnected from API.")
while retry_timer:
retry_timer.pop(0).cancel()
error = None
try:
cli.connect()
cli.login()
except APIConnectionError as error:
pass
if error is None:
_LOGGER.info("Successfully connected to %s", address)
return
wait_time = min(2**tries, 300)
_LOGGER.warning(u"Couldn't connect to API. Trying to reconnect in %s seconds", wait_time)
timer = threading.Timer(wait_time, functools.partial(try_connect, tries + 1, is_disconnect))
timer.start()
retry_timer.append(timer)
def on_log(msg):
time_ = datetime.now().time().strftime(u'[%H:%M:%S]')
safe_print(time_ + msg.message)
has_connects = []
def on_login():
try:
cli.subscribe_logs(on_log, dump_config=not has_connects)
has_connects.append(True)
except APIConnectionError:
cli.disconnect()
cli.on_disconnect = try_connect
cli.on_login = on_login
cli.start()
try:
try_connect(is_disconnect=False)
while True:
time.sleep(1)
except KeyboardInterrupt:
stopping = True
cli.stop(True)
while retry_timer:
retry_timer.pop(0).cancel()
return 0

View File

@ -27,7 +27,7 @@ def maybe_simple_id(*validators):
def validate_recursive_condition(value): def validate_recursive_condition(value):
is_list = isinstance(value, list) is_list = isinstance(value, list)
value = cv.ensure_list(value)[:] value = cv.ensure_list()(value)[:]
for i, item in enumerate(value): for i, item in enumerate(value):
path = [i] if is_list else [] path = [i] if is_list else []
item = copy.deepcopy(item) item = copy.deepcopy(item)
@ -61,7 +61,8 @@ def validate_recursive_condition(value):
def validate_recursive_action(value): def validate_recursive_action(value):
is_list = isinstance(value, list) is_list = isinstance(value, list)
value = cv.ensure_list(value)[:] if not is_list:
value = [value]
for i, item in enumerate(value): for i, item in enumerate(value):
path = [i] if is_list else [] path = [i] if is_list else []
item = copy.deepcopy(item) item = copy.deepcopy(item)

View File

@ -0,0 +1,85 @@
import voluptuous as vol
from esphomeyaml.automation import ACTION_REGISTRY
import esphomeyaml.config_validation as cv
from esphomeyaml.const import CONF_DATA, CONF_DATA_TEMPLATE, CONF_ID, CONF_PASSWORD, CONF_PORT, \
CONF_SERVICE, CONF_VARIABLES
from esphomeyaml.core import CORE
from esphomeyaml.cpp_generator import ArrayInitializer, Pvariable, add, get_variable, process_lambda
from esphomeyaml.cpp_helpers import setup_component
from esphomeyaml.cpp_types import Action, App, Component, StoringController, esphomelib_ns
api_ns = esphomelib_ns.namespace('api')
APIServer = api_ns.class_('APIServer', Component, StoringController)
HomeAssistantServiceCallAction = api_ns.class_('HomeAssistantServiceCallAction', Action)
KeyValuePair = api_ns.class_('KeyValuePair')
TemplatableKeyValuePair = api_ns.class_('TemplatableKeyValuePair')
CONFIG_SCHEMA = vol.Schema({
cv.GenerateID(): cv.declare_variable_id(APIServer),
vol.Optional(CONF_PORT, default=6053): cv.port,
vol.Optional(CONF_PASSWORD, default=''): cv.string_strict,
}).extend(cv.COMPONENT_SCHEMA.schema)
def to_code(config):
rhs = App.init_api_server()
api = Pvariable(config[CONF_ID], rhs)
if config[CONF_PORT] != 6053:
add(api.set_port(config[CONF_PORT]))
if config.get(CONF_PASSWORD):
add(api.set_password(config[CONF_PASSWORD]))
setup_component(api, config)
BUILD_FLAGS = '-DUSE_API'
def lib_deps(config):
if CORE.is_esp32:
return 'AsyncTCP@1.0.1'
elif CORE.is_esp8266:
return 'ESPAsyncTCP@1.1.3'
raise NotImplementedError
CONF_HOMEASSISTANT_SERVICE = 'homeassistant.service'
LOGGER_LOG_ACTION_SCHEMA = vol.Schema({
cv.GenerateID(): cv.use_variable_id(APIServer),
vol.Required(CONF_SERVICE): cv.string,
vol.Optional(CONF_DATA): vol.Schema({
cv.string: cv.string,
}),
vol.Optional(CONF_DATA_TEMPLATE): vol.Schema({
cv.string: cv.string,
}),
vol.Optional(CONF_VARIABLES): vol.Schema({
cv.string: cv.lambda_,
}),
})
@ACTION_REGISTRY.register(CONF_HOMEASSISTANT_SERVICE, LOGGER_LOG_ACTION_SCHEMA)
def homeassistant_service_to_code(config, action_id, arg_type, template_arg):
for var in get_variable(config[CONF_ID]):
yield None
rhs = var.make_home_assistant_service_call_action(template_arg)
type = HomeAssistantServiceCallAction.template(arg_type)
act = Pvariable(action_id, rhs, type=type)
add(act.set_service(config[CONF_SERVICE]))
if CONF_DATA in config:
datas = [KeyValuePair(k, v) for k, v in config[CONF_DATA].items()]
add(act.set_data(ArrayInitializer(*datas)))
if CONF_DATA_TEMPLATE in config:
datas = [KeyValuePair(k, v) for k, v in config[CONF_DATA_TEMPLATE].items()]
add(act.set_data_template(ArrayInitializer(*datas)))
if CONF_VARIABLES in config:
datas = []
for key, value in config[CONF_VARIABLES].items():
for value_ in process_lambda(value, []):
yield None
datas.append(TemplatableKeyValuePair(key, value_))
add(act.set_variables(ArrayInitializer(*datas)))
yield act

View File

@ -9,7 +9,7 @@ from esphomeyaml.const import CONF_DELAYED_OFF, CONF_DELAYED_ON, CONF_DEVICE_CLA
CONF_HEARTBEAT, CONF_ID, CONF_INTERNAL, CONF_INVALID_COOLDOWN, CONF_INVERT, CONF_INVERTED, \ CONF_HEARTBEAT, CONF_ID, CONF_INTERNAL, CONF_INVALID_COOLDOWN, CONF_INVERT, CONF_INVERTED, \
CONF_LAMBDA, CONF_MAX_LENGTH, CONF_MIN_LENGTH, CONF_MQTT_ID, CONF_ON_CLICK, \ CONF_LAMBDA, CONF_MAX_LENGTH, CONF_MIN_LENGTH, CONF_MQTT_ID, CONF_ON_CLICK, \
CONF_ON_DOUBLE_CLICK, CONF_ON_MULTI_CLICK, CONF_ON_PRESS, CONF_ON_RELEASE, CONF_STATE, \ CONF_ON_DOUBLE_CLICK, CONF_ON_MULTI_CLICK, CONF_ON_PRESS, CONF_ON_RELEASE, CONF_STATE, \
CONF_TIMING, CONF_TRIGGER_ID CONF_TIMING, CONF_TRIGGER_ID, CONF_ON_STATE
from esphomeyaml.core import CORE from esphomeyaml.core import CORE
from esphomeyaml.cpp_generator import process_lambda, ArrayInitializer, add, Pvariable, \ from esphomeyaml.cpp_generator import process_lambda, ArrayInitializer, add, Pvariable, \
StructInitializer, get_variable StructInitializer, get_variable
@ -38,6 +38,7 @@ ClickTrigger = binary_sensor_ns.class_('ClickTrigger', Trigger.template(NoArg))
DoubleClickTrigger = binary_sensor_ns.class_('DoubleClickTrigger', Trigger.template(NoArg)) DoubleClickTrigger = binary_sensor_ns.class_('DoubleClickTrigger', Trigger.template(NoArg))
MultiClickTrigger = binary_sensor_ns.class_('MultiClickTrigger', Trigger.template(NoArg), Component) MultiClickTrigger = binary_sensor_ns.class_('MultiClickTrigger', Trigger.template(NoArg), Component)
MultiClickTriggerEvent = binary_sensor_ns.struct('MultiClickTriggerEvent') MultiClickTriggerEvent = binary_sensor_ns.struct('MultiClickTriggerEvent')
StateTrigger = binary_sensor_ns.class_('StateTrigger', Trigger.template(bool_))
# Condition # Condition
BinarySensorCondition = binary_sensor_ns.class_('BinarySensorCondition', Condition) BinarySensorCondition = binary_sensor_ns.class_('BinarySensorCondition', Condition)
@ -53,13 +54,13 @@ LambdaFilter = binary_sensor_ns.class_('LambdaFilter', Filter)
FILTER_KEYS = [CONF_INVERT, CONF_DELAYED_ON, CONF_DELAYED_OFF, CONF_LAMBDA, CONF_HEARTBEAT] FILTER_KEYS = [CONF_INVERT, CONF_DELAYED_ON, CONF_DELAYED_OFF, CONF_LAMBDA, CONF_HEARTBEAT]
FILTERS_SCHEMA = vol.All(cv.ensure_list, [vol.All({ FILTERS_SCHEMA = cv.ensure_list({
vol.Optional(CONF_INVERT): None, vol.Optional(CONF_INVERT): None,
vol.Optional(CONF_DELAYED_ON): cv.positive_time_period_milliseconds, vol.Optional(CONF_DELAYED_ON): cv.positive_time_period_milliseconds,
vol.Optional(CONF_DELAYED_OFF): cv.positive_time_period_milliseconds, vol.Optional(CONF_DELAYED_OFF): cv.positive_time_period_milliseconds,
vol.Optional(CONF_HEARTBEAT): cv.positive_time_period_milliseconds, vol.Optional(CONF_HEARTBEAT): cv.positive_time_period_milliseconds,
vol.Optional(CONF_LAMBDA): cv.lambda_, vol.Optional(CONF_LAMBDA): cv.lambda_,
}, cv.has_exactly_one_key(*FILTER_KEYS))]) }, cv.has_exactly_one_key(*FILTER_KEYS))
MULTI_CLICK_TIMING_SCHEMA = vol.Schema({ MULTI_CLICK_TIMING_SCHEMA = vol.Schema({
vol.Optional(CONF_STATE): cv.boolean, vol.Optional(CONF_STATE): cv.boolean,
@ -181,6 +182,9 @@ BINARY_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend({
validate_multi_click_timing), validate_multi_click_timing),
vol.Optional(CONF_INVALID_COOLDOWN): cv.positive_time_period_milliseconds, vol.Optional(CONF_INVALID_COOLDOWN): cv.positive_time_period_milliseconds,
}), }),
vol.Optional(CONF_ON_STATE): automation.validate_automation({
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(StateTrigger),
}),
vol.Optional(CONF_INVERTED): cv.invalid( vol.Optional(CONF_INVERTED): cv.invalid(
"The inverted binary_sensor property has been replaced by the " "The inverted binary_sensor property has been replaced by the "
@ -268,6 +272,11 @@ def setup_binary_sensor_core_(binary_sensor_var, mqtt_var, config):
add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN])) add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN]))
automation.build_automation(trigger, NoArg, conf) automation.build_automation(trigger, NoArg, conf)
for conf in config.get(CONF_ON_STATE, []):
rhs = binary_sensor_var.make_state_trigger()
trigger = Pvariable(conf[CONF_TRIGGER_ID], rhs)
automation.build_automation(trigger, bool_, conf)
setup_mqtt_component(mqtt_var, config) setup_mqtt_component(mqtt_var, config)

View File

@ -13,9 +13,9 @@ PLATFORM_SCHEMA = binary_sensor.PLATFORM_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(CustomBinarySensorConstructor), cv.GenerateID(): cv.declare_variable_id(CustomBinarySensorConstructor),
vol.Required(CONF_LAMBDA): cv.lambda_, vol.Required(CONF_LAMBDA): cv.lambda_,
vol.Required(CONF_BINARY_SENSORS): vol.Required(CONF_BINARY_SENSORS):
vol.All(cv.ensure_list, [binary_sensor.BINARY_SENSOR_SCHEMA.extend({ cv.ensure_list(binary_sensor.BINARY_SENSOR_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(binary_sensor.BinarySensor), cv.GenerateID(): cv.declare_variable_id(binary_sensor.BinarySensor),
})]), })),
}) })

View File

@ -5,7 +5,6 @@ from esphomeyaml.cpp_generator import variable
from esphomeyaml.cpp_helpers import setup_component from esphomeyaml.cpp_helpers import setup_component
from esphomeyaml.cpp_types import Application, Component, App from esphomeyaml.cpp_types import Application, Component, App
DEPENDENCIES = ['mqtt']
MakeStatusBinarySensor = Application.struct('MakeStatusBinarySensor') MakeStatusBinarySensor = Application.struct('MakeStatusBinarySensor')
StatusBinarySensor = binary_sensor.binary_sensor_ns.class_('StatusBinarySensor', StatusBinarySensor = binary_sensor.binary_sensor_ns.class_('StatusBinarySensor',

View File

@ -12,9 +12,9 @@ MULTI_CONF = True
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
cv.GenerateID(): cv.declare_variable_id(CustomComponentConstructor), cv.GenerateID(): cv.declare_variable_id(CustomComponentConstructor),
vol.Required(CONF_LAMBDA): cv.lambda_, vol.Required(CONF_LAMBDA): cv.lambda_,
vol.Optional(CONF_COMPONENTS): vol.All(cv.ensure_list, [vol.Schema({ vol.Optional(CONF_COMPONENTS): cv.ensure_list(vol.Schema({
cv.GenerateID(): cv.declare_variable_id(Component) cv.GenerateID(): cv.declare_variable_id(Component)
}).extend(cv.COMPONENT_SCHEMA.schema)]), }).extend(cv.COMPONENT_SCHEMA.schema)),
}) })

View File

@ -46,8 +46,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_WAKEUP_PIN_MODE): vol.All(cv.only_on_esp32, vol.Optional(CONF_WAKEUP_PIN_MODE): vol.All(cv.only_on_esp32,
cv.one_of(*WAKEUP_PIN_MODES), upper=True), cv.one_of(*WAKEUP_PIN_MODES), upper=True),
vol.Optional(CONF_ESP32_EXT1_WAKEUP): vol.All(cv.only_on_esp32, vol.Schema({ vol.Optional(CONF_ESP32_EXT1_WAKEUP): vol.All(cv.only_on_esp32, vol.Schema({
vol.Required(CONF_PINS): vol.All(cv.ensure_list, [pins.shorthand_input_pin], vol.Required(CONF_PINS): cv.ensure_list(pins.shorthand_input_pin, validate_pin_number),
[validate_pin_number]),
vol.Required(CONF_MODE): cv.one_of(*EXT1_WAKEUP_MODES, upper=True), vol.Required(CONF_MODE): cv.one_of(*EXT1_WAKEUP_MODES, upper=True),
})), })),
vol.Optional(CONF_RUN_CYCLES): cv.positive_int, vol.Optional(CONF_RUN_CYCLES): cv.positive_int,

View File

@ -0,0 +1,23 @@
import voluptuous as vol
from esphomeyaml import automation
import esphomeyaml.config_validation as cv
from esphomeyaml.const import CONF_ID
from esphomeyaml.cpp_generator import Pvariable
from esphomeyaml.cpp_helpers import setup_component
from esphomeyaml.cpp_types import App, NoArg, PollingComponent, Trigger, esphomelib_ns
IntervalTrigger = esphomelib_ns.class_('IntervalTrigger', Trigger.template(NoArg), PollingComponent)
CONFIG_SCHEMA = automation.validate_automation(vol.Schema({
vol.Required(CONF_ID): cv.declare_variable_id(IntervalTrigger),
}).extend(cv.COMPONENT_SCHEMA.schema))
def to_code(config):
for conf in config:
rhs = App.register_component(IntervalTrigger.new())
trigger = Pvariable(conf[CONF_ID], rhs)
setup_component(trigger, conf)
automation.build_automation(trigger, NoArg, conf)

View File

@ -100,7 +100,7 @@ EFFECTS_SCHEMA = vol.Schema({
vol.Optional(CONF_STROBE): vol.Schema({ vol.Optional(CONF_STROBE): vol.Schema({
cv.GenerateID(CONF_EFFECT_ID): cv.declare_variable_id(StrobeLightEffect), cv.GenerateID(CONF_EFFECT_ID): cv.declare_variable_id(StrobeLightEffect),
vol.Optional(CONF_NAME, default="Strobe"): cv.string, vol.Optional(CONF_NAME, default="Strobe"): cv.string,
vol.Optional(CONF_COLORS): vol.All(cv.ensure_list, [vol.All(vol.Schema({ vol.Optional(CONF_COLORS): vol.All(cv.ensure_list(vol.Schema({
vol.Optional(CONF_STATE, default=True): cv.boolean, vol.Optional(CONF_STATE, default=True): cv.boolean,
vol.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, vol.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage,
vol.Optional(CONF_RED, default=1.0): cv.percentage, vol.Optional(CONF_RED, default=1.0): cv.percentage,
@ -109,7 +109,7 @@ EFFECTS_SCHEMA = vol.Schema({
vol.Optional(CONF_WHITE, default=1.0): cv.percentage, vol.Optional(CONF_WHITE, default=1.0): cv.percentage,
vol.Required(CONF_DURATION): cv.positive_time_period_milliseconds, vol.Required(CONF_DURATION): cv.positive_time_period_milliseconds,
}), cv.has_at_least_one_key(CONF_STATE, CONF_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, }), cv.has_at_least_one_key(CONF_STATE, CONF_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE,
CONF_WHITE))], vol.Length(min=2)), CONF_WHITE)), vol.Length(min=2)),
}), }),
vol.Optional(CONF_FLICKER): vol.Schema({ vol.Optional(CONF_FLICKER): vol.Schema({
cv.GenerateID(CONF_EFFECT_ID): cv.declare_variable_id(FlickerLightEffect), cv.GenerateID(CONF_EFFECT_ID): cv.declare_variable_id(FlickerLightEffect),
@ -131,13 +131,13 @@ EFFECTS_SCHEMA = vol.Schema({
vol.Optional(CONF_FASTLED_COLOR_WIPE): vol.Schema({ vol.Optional(CONF_FASTLED_COLOR_WIPE): vol.Schema({
cv.GenerateID(CONF_EFFECT_ID): cv.declare_variable_id(FastLEDColorWipeEffect), cv.GenerateID(CONF_EFFECT_ID): cv.declare_variable_id(FastLEDColorWipeEffect),
vol.Optional(CONF_NAME, default="Color Wipe"): cv.string, vol.Optional(CONF_NAME, default="Color Wipe"): cv.string,
vol.Optional(CONF_COLORS): vol.All(cv.ensure_list, [vol.Schema({ vol.Optional(CONF_COLORS): cv.ensure_list({
vol.Optional(CONF_RED, default=1.0): cv.percentage, vol.Optional(CONF_RED, default=1.0): cv.percentage,
vol.Optional(CONF_GREEN, default=1.0): cv.percentage, vol.Optional(CONF_GREEN, default=1.0): cv.percentage,
vol.Optional(CONF_BLUE, default=1.0): cv.percentage, vol.Optional(CONF_BLUE, default=1.0): cv.percentage,
vol.Optional(CONF_RANDOM, default=False): cv.boolean, vol.Optional(CONF_RANDOM, default=False): cv.boolean,
vol.Required(CONF_NUM_LEDS): vol.All(cv.uint32_t, vol.Range(min=1)), vol.Required(CONF_NUM_LEDS): vol.All(cv.uint32_t, vol.Range(min=1)),
})]), }),
vol.Optional(CONF_ADD_LED_INTERVAL): cv.positive_time_period_milliseconds, vol.Optional(CONF_ADD_LED_INTERVAL): cv.positive_time_period_milliseconds,
vol.Optional(CONF_REVERSE): cv.boolean, vol.Optional(CONF_REVERSE): cv.boolean,
}), }),
@ -178,7 +178,8 @@ EFFECTS_SCHEMA = vol.Schema({
def validate_effects(allowed_effects): def validate_effects(allowed_effects):
def validator(value): def validator(value):
is_list = isinstance(value, list) is_list = isinstance(value, list)
value = cv.ensure_list(value) if not is_list:
value = [value]
names = set() names = set()
ret = [] ret = []
for i, effect in enumerate(value): for i, effect in enumerate(value):
@ -471,10 +472,10 @@ def light_turn_on_to_code(config, action_id, arg_type, template_arg):
def core_to_hass_config(data, config, brightness=True, rgb=True, color_temp=True, def core_to_hass_config(data, config, brightness=True, rgb=True, color_temp=True,
white_value=True): white_value=True):
ret = mqtt.build_hass_config(data, 'light', config, include_state=True, include_command=True, ret = mqtt.build_hass_config(data, 'light', config, include_state=True, include_command=True)
platform='mqtt_json')
if ret is None: if ret is None:
return None return None
ret['schema'] = 'json'
if brightness: if brightness:
ret['brightness'] = True ret['brightness'] = True
if rgb: if rgb:

View File

@ -108,7 +108,7 @@ def validate_printf(value):
CONF_LOGGER_LOG = 'logger.log' CONF_LOGGER_LOG = 'logger.log'
LOGGER_LOG_ACTION_SCHEMA = vol.All(maybe_simple_message({ LOGGER_LOG_ACTION_SCHEMA = vol.All(maybe_simple_message({
vol.Required(CONF_FORMAT): cv.string, vol.Required(CONF_FORMAT): cv.string,
vol.Optional(CONF_ARGS, default=list): vol.All(cv.ensure_list, [cv.lambda_]), vol.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_),
vol.Optional(CONF_LEVEL, default="DEBUG"): cv.one_of(*LOG_LEVEL_TO_ESP_LOG, upper=True), vol.Optional(CONF_LEVEL, default="DEBUG"): cv.one_of(*LOG_LEVEL_TO_ESP_LOG, upper=True),
vol.Optional(CONF_TAG, default="main"): cv.string, vol.Optional(CONF_TAG, default="main"): cv.string,
}), validate_printf) }), validate_printf)

View File

@ -85,7 +85,7 @@ CONFIG_SCHEMA = vol.All(vol.Schema({
vol.Optional(CONF_LEVEL): logger.is_log_level, vol.Optional(CONF_LEVEL): logger.is_log_level,
}), validate_message_just_topic), }), validate_message_just_topic),
vol.Optional(CONF_SSL_FINGERPRINTS): vol.All(cv.only_on_esp8266, vol.Optional(CONF_SSL_FINGERPRINTS): vol.All(cv.only_on_esp8266,
cv.ensure_list, [validate_fingerprint]), cv.ensure_list(validate_fingerprint)),
vol.Optional(CONF_KEEPALIVE): cv.positive_time_period_seconds, vol.Optional(CONF_KEEPALIVE): cv.positive_time_period_seconds,
vol.Optional(CONF_REBOOT_TIMEOUT): cv.positive_time_period_milliseconds, vol.Optional(CONF_REBOOT_TIMEOUT): cv.positive_time_period_milliseconds,
vol.Optional(CONF_ON_MESSAGE): automation.validate_automation({ vol.Optional(CONF_ON_MESSAGE): automation.validate_automation({
@ -260,12 +260,11 @@ def get_default_topic_for(data, component_type, name, suffix):
sanitized_name, suffix) sanitized_name, suffix)
def build_hass_config(data, component_type, config, include_state=True, include_command=True, def build_hass_config(data, component_type, config, include_state=True, include_command=True):
platform='mqtt'):
if config.get(CONF_INTERNAL, False): if config.get(CONF_INTERNAL, False):
return None return None
ret = OrderedDict() ret = OrderedDict()
ret['platform'] = platform ret['platform'] = 'mqtt'
ret['name'] = config[CONF_NAME] ret['name'] = config[CONF_NAME]
if include_state: if include_state:
default = get_default_topic_for(data, component_type, config[CONF_NAME], 'state') default = get_default_topic_for(data, component_type, config[CONF_NAME], 'state')

View File

@ -13,18 +13,18 @@ BINARY_SCHEMA = output.PLATFORM_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(CustomBinaryOutputConstructor), cv.GenerateID(): cv.declare_variable_id(CustomBinaryOutputConstructor),
vol.Required(CONF_LAMBDA): cv.lambda_, vol.Required(CONF_LAMBDA): cv.lambda_,
vol.Required(CONF_OUTPUTS): vol.Required(CONF_OUTPUTS):
vol.All(cv.ensure_list, [output.BINARY_OUTPUT_SCHEMA.extend({ cv.ensure_list(output.BINARY_OUTPUT_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(output.BinaryOutput), cv.GenerateID(): cv.declare_variable_id(output.BinaryOutput),
})]), })),
}) })
FLOAT_SCHEMA = output.PLATFORM_SCHEMA.extend({ FLOAT_SCHEMA = output.PLATFORM_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(CustomFloatOutputConstructor), cv.GenerateID(): cv.declare_variable_id(CustomFloatOutputConstructor),
vol.Required(CONF_LAMBDA): cv.lambda_, vol.Required(CONF_LAMBDA): cv.lambda_,
vol.Required(CONF_OUTPUTS): vol.Required(CONF_OUTPUTS):
vol.All(cv.ensure_list, [output.FLOAT_OUTPUT_PLATFORM_SCHEMA.extend({ cv.ensure_list(output.FLOAT_OUTPUT_PLATFORM_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(output.FloatOutput), cv.GenerateID(): cv.declare_variable_id(output.FloatOutput),
})]), })),
}) })

View File

@ -41,8 +41,7 @@ CONFIG_SCHEMA = vol.Schema({
cv.GenerateID(): cv.declare_variable_id(RemoteReceiverComponent), cv.GenerateID(): cv.declare_variable_id(RemoteReceiverComponent),
vol.Required(CONF_PIN): pins.gpio_input_pin_schema, vol.Required(CONF_PIN): pins.gpio_input_pin_schema,
vol.Optional(CONF_DUMP, default=[]): vol.Optional(CONF_DUMP, default=[]):
vol.Any(validate_dumpers_all, vol.Any(validate_dumpers_all, cv.ensure_list(cv.one_of(*DUMPERS, lower=True))),
vol.All(cv.ensure_list, [cv.one_of(*DUMPERS, lower=True)])),
vol.Optional(CONF_TOLERANCE): vol.All(cv.percentage_int, vol.Range(min=0)), vol.Optional(CONF_TOLERANCE): vol.All(cv.percentage_int, vol.Range(min=0)),
vol.Optional(CONF_BUFFER_SIZE): cv.validate_bytes, vol.Optional(CONF_BUFFER_SIZE): cv.validate_bytes,
vol.Optional(CONF_FILTER): cv.positive_time_period_microseconds, vol.Optional(CONF_FILTER): cv.positive_time_period_microseconds,

View File

@ -40,7 +40,7 @@ FILTER_KEYS = [CONF_OFFSET, CONF_MULTIPLY, CONF_FILTER_OUT, CONF_FILTER_NAN,
CONF_SLIDING_WINDOW_MOVING_AVERAGE, CONF_EXPONENTIAL_MOVING_AVERAGE, CONF_LAMBDA, CONF_SLIDING_WINDOW_MOVING_AVERAGE, CONF_EXPONENTIAL_MOVING_AVERAGE, CONF_LAMBDA,
CONF_THROTTLE, CONF_DELTA, CONF_UNIQUE, CONF_HEARTBEAT, CONF_DEBOUNCE, CONF_OR] CONF_THROTTLE, CONF_DELTA, CONF_UNIQUE, CONF_HEARTBEAT, CONF_DEBOUNCE, CONF_OR]
FILTERS_SCHEMA = vol.All(cv.ensure_list, [vol.All({ FILTERS_SCHEMA = cv.ensure_list({
vol.Optional(CONF_OFFSET): cv.float_, vol.Optional(CONF_OFFSET): cv.float_,
vol.Optional(CONF_MULTIPLY): cv.float_, vol.Optional(CONF_MULTIPLY): cv.float_,
vol.Optional(CONF_FILTER_OUT): cv.float_, vol.Optional(CONF_FILTER_OUT): cv.float_,
@ -61,7 +61,7 @@ FILTERS_SCHEMA = vol.All(cv.ensure_list, [vol.All({
vol.Optional(CONF_HEARTBEAT): cv.positive_time_period_milliseconds, vol.Optional(CONF_HEARTBEAT): cv.positive_time_period_milliseconds,
vol.Optional(CONF_DEBOUNCE): cv.positive_time_period_milliseconds, vol.Optional(CONF_DEBOUNCE): cv.positive_time_period_milliseconds,
vol.Optional(CONF_OR): validate_recursive_filter, vol.Optional(CONF_OR): validate_recursive_filter,
}, cv.has_exactly_one_key(*FILTER_KEYS))]) }, cv.has_exactly_one_key(*FILTER_KEYS))
# Base # Base
sensor_ns = esphomelib_ns.namespace('sensor') sensor_ns = esphomelib_ns.namespace('sensor')

View File

@ -11,9 +11,9 @@ CustomSensorConstructor = sensor.sensor_ns.class_('CustomSensorConstructor')
PLATFORM_SCHEMA = sensor.PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = sensor.PLATFORM_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(CustomSensorConstructor), cv.GenerateID(): cv.declare_variable_id(CustomSensorConstructor),
vol.Required(CONF_LAMBDA): cv.lambda_, vol.Required(CONF_LAMBDA): cv.lambda_,
vol.Required(CONF_SENSORS): vol.All(cv.ensure_list, [sensor.SENSOR_SCHEMA.extend({ vol.Required(CONF_SENSORS): cv.ensure_list(sensor.SENSOR_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(sensor.Sensor), cv.GenerateID(): cv.declare_variable_id(sensor.Sensor),
})]), })),
}) })

View File

@ -0,0 +1,32 @@
import voluptuous as vol
from esphomeyaml.components import sensor
import esphomeyaml.config_validation as cv
from esphomeyaml.const import CONF_ENTITY_ID, CONF_MAKE_ID, CONF_NAME
from esphomeyaml.cpp_generator import variable
from esphomeyaml.cpp_types import App, Application
DEPENDENCIES = ['api']
MakeHomeassistantSensor = Application.struct('MakeHomeassistantSensor')
HomeassistantSensor = sensor.sensor_ns.class_('HomeassistantSensor', sensor.Sensor)
PLATFORM_SCHEMA = cv.nameable(sensor.SENSOR_PLATFORM_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(HomeassistantSensor),
cv.GenerateID(CONF_MAKE_ID): cv.declare_variable_id(MakeHomeassistantSensor),
vol.Required(CONF_ENTITY_ID): cv.entity_id,
}))
def to_code(config):
rhs = App.make_homeassistant_sensor(config[CONF_NAME], config[CONF_ENTITY_ID])
make = variable(config[CONF_MAKE_ID], rhs)
subs = make.Psensor
sensor.setup_sensor(subs, make.Pmqtt, config)
BUILD_FLAGS = '-DUSE_HOMEASSISTANT_SENSOR'
def to_hass_config(data, config):
return sensor.core_to_hass_config(data, config)

View File

@ -12,9 +12,9 @@ PLATFORM_SCHEMA = switch.PLATFORM_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(CustomSwitchConstructor), cv.GenerateID(): cv.declare_variable_id(CustomSwitchConstructor),
vol.Required(CONF_LAMBDA): cv.lambda_, vol.Required(CONF_LAMBDA): cv.lambda_,
vol.Required(CONF_SWITCHES): vol.Required(CONF_SWITCHES):
vol.All(cv.ensure_list, [switch.SWITCH_SCHEMA.extend({ cv.ensure_list(switch.SWITCH_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(switch.Switch), cv.GenerateID(): cv.declare_variable_id(switch.Switch),
})]), })),
}) })

View File

@ -12,9 +12,9 @@ PLATFORM_SCHEMA = text_sensor.PLATFORM_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(CustomTextSensorConstructor), cv.GenerateID(): cv.declare_variable_id(CustomTextSensorConstructor),
vol.Required(CONF_LAMBDA): cv.lambda_, vol.Required(CONF_LAMBDA): cv.lambda_,
vol.Required(CONF_TEXT_SENSORS): vol.Required(CONF_TEXT_SENSORS):
vol.All(cv.ensure_list, [text_sensor.TEXT_SENSOR_SCHEMA.extend({ cv.ensure_list(text_sensor.TEXT_SENSOR_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(text_sensor.TextSensor), cv.GenerateID(): cv.declare_variable_id(text_sensor.TextSensor),
})]), })),
}) })

View File

@ -0,0 +1,33 @@
import voluptuous as vol
from esphomeyaml.components import text_sensor
import esphomeyaml.config_validation as cv
from esphomeyaml.const import CONF_ENTITY_ID, CONF_MAKE_ID, CONF_NAME
from esphomeyaml.cpp_generator import variable
from esphomeyaml.cpp_types import App, Application, Component
DEPENDENCIES = ['api']
MakeHomeassistantTextSensor = Application.struct('MakeHomeassistantTextSensor')
HomeassistantTextSensor = text_sensor.text_sensor_ns.class_('HomeassistantTextSensor',
text_sensor.TextSensor, Component)
PLATFORM_SCHEMA = cv.nameable(text_sensor.TEXT_SENSOR_PLATFORM_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(HomeassistantTextSensor),
cv.GenerateID(CONF_MAKE_ID): cv.declare_variable_id(MakeHomeassistantTextSensor),
vol.Required(CONF_ENTITY_ID): cv.entity_id,
}))
def to_code(config):
rhs = App.make_homeassistant_text_sensor(config[CONF_NAME], config[CONF_ENTITY_ID])
make = variable(config[CONF_MAKE_ID], rhs)
sensor_ = make.Psensor
text_sensor.setup_text_sensor(sensor_, make.Pmqtt, config)
BUILD_FLAGS = '-DUSE_HOMEASSISTANT_TEXT_SENSOR'
def to_hass_config(data, config):
return text_sensor.core_to_hass_config(data, config)

View File

@ -0,0 +1,25 @@
from esphomeyaml.components import time as time_
import esphomeyaml.config_validation as cv
from esphomeyaml.const import CONF_ID
from esphomeyaml.cpp_generator import Pvariable
from esphomeyaml.cpp_helpers import setup_component
from esphomeyaml.cpp_types import App
DEPENDENCIES = ['api']
HomeAssistantTime = time_.time_ns.class_('HomeAssistantTime', time_.RealTimeClockComponent)
PLATFORM_SCHEMA = time_.TIME_PLATFORM_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(HomeAssistantTime),
}).extend(cv.COMPONENT_SCHEMA.schema)
def to_code(config):
rhs = App.make_homeassistant_time_component()
ha_time = Pvariable(config[CONF_ID], rhs)
time_.setup_time(ha_time, config)
setup_component(ha_time, config)
BUILD_FLAGS = '-DUSE_HOMEASSISTANT_TIME'

View File

@ -1,8 +1,8 @@
import voluptuous as vol import voluptuous as vol
import esphomeyaml.config_validation as cv
from esphomeyaml.components import time as time_ from esphomeyaml.components import time as time_
from esphomeyaml.const import CONF_ID, CONF_LAMBDA, CONF_SERVERS import esphomeyaml.config_validation as cv
from esphomeyaml.const import CONF_ID, CONF_SERVERS
from esphomeyaml.cpp_generator import Pvariable, add from esphomeyaml.cpp_generator import Pvariable, add
from esphomeyaml.cpp_helpers import setup_component from esphomeyaml.cpp_helpers import setup_component
from esphomeyaml.cpp_types import App from esphomeyaml.cpp_types import App
@ -11,8 +11,7 @@ SNTPComponent = time_.time_ns.class_('SNTPComponent', time_.RealTimeClockCompone
PLATFORM_SCHEMA = time_.TIME_PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = time_.TIME_PLATFORM_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(SNTPComponent), cv.GenerateID(): cv.declare_variable_id(SNTPComponent),
vol.Optional(CONF_SERVERS): vol.All(cv.ensure_list, [cv.domain], vol.Length(min=1, max=3)), vol.Optional(CONF_SERVERS): vol.All(cv.ensure_list(cv.domain), vol.Length(min=1, max=3)),
vol.Optional(CONF_LAMBDA): cv.lambda_,
}).extend(cv.COMPONENT_SCHEMA.schema) }).extend(cv.COMPONENT_SCHEMA.schema)

View File

@ -3,12 +3,25 @@ import voluptuous as vol
import esphomeyaml.config_validation as cv import esphomeyaml.config_validation as cv
from esphomeyaml.const import CONF_AP, CONF_CHANNEL, CONF_DNS1, CONF_DNS2, CONF_DOMAIN, \ from esphomeyaml.const import CONF_AP, CONF_CHANNEL, CONF_DNS1, CONF_DNS2, CONF_DOMAIN, \
CONF_GATEWAY, CONF_HOSTNAME, CONF_ID, CONF_MANUAL_IP, CONF_PASSWORD, CONF_POWER_SAVE_MODE, \ CONF_GATEWAY, CONF_HOSTNAME, CONF_ID, CONF_MANUAL_IP, CONF_PASSWORD, CONF_POWER_SAVE_MODE, \
CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, CONF_SUBNET CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, CONF_SUBNET, CONF_NETWORKS, CONF_BSSID
from esphomeyaml.core import CORE from esphomeyaml.core import CORE, HexInt
from esphomeyaml.cpp_generator import Pvariable, StructInitializer, add from esphomeyaml.cpp_generator import Pvariable, StructInitializer, add, variable, ArrayInitializer
from esphomeyaml.cpp_types import App, Component, esphomelib_ns, global_ns from esphomeyaml.cpp_types import App, Component, esphomelib_ns, global_ns
IPAddress = global_ns.class_('IPAddress')
ManualIP = esphomelib_ns.struct('ManualIP')
WiFiComponent = esphomelib_ns.class_('WiFiComponent', Component)
WiFiAP = esphomelib_ns.struct('WiFiAP')
WiFiPowerSaveMode = esphomelib_ns.enum('WiFiPowerSaveMode')
WIFI_POWER_SAVE_MODES = {
'NONE': WiFiPowerSaveMode.WIFI_POWER_SAVE_NONE,
'LIGHT': WiFiPowerSaveMode.WIFI_POWER_SAVE_LIGHT,
'HIGH': WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH,
}
def validate_password(value): def validate_password(value):
value = cv.string(value) value = cv.string(value)
if not value: if not value:
@ -41,10 +54,11 @@ STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend({
}) })
WIFI_NETWORK_BASE = vol.Schema({ WIFI_NETWORK_BASE = vol.Schema({
vol.Required(CONF_SSID): cv.ssid, cv.GenerateID(): cv.declare_variable_id(WiFiAP),
vol.Optional(CONF_SSID): cv.ssid,
vol.Optional(CONF_PASSWORD): validate_password, vol.Optional(CONF_PASSWORD): validate_password,
vol.Optional(CONF_CHANNEL): validate_channel, vol.Optional(CONF_CHANNEL): validate_channel,
vol.Optional(CONF_MANUAL_IP): AP_MANUAL_IP_SCHEMA, vol.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA,
}) })
WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend({ WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend({
@ -53,35 +67,39 @@ WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend({
WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend({ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend({
vol.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, vol.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA,
vol.Optional(CONF_BSSID): cv.mac_address,
}) })
def validate(config): def validate(config):
if CONF_PASSWORD in config and CONF_SSID not in config: if CONF_PASSWORD in config and CONF_SSID not in config:
raise vol.Invalid("Cannot have WiFi password without SSID!") raise vol.Invalid("Cannot have WiFi password without SSID!")
if (CONF_SSID not in config) and (CONF_AP not in config):
if CONF_SSID in config:
network = {CONF_SSID: config.pop(CONF_SSID)}
if CONF_PASSWORD in config:
network[CONF_PASSWORD] = config.pop(CONF_PASSWORD)
if CONF_MANUAL_IP in config:
network[CONF_MANUAL_IP] = config.pop(CONF_MANUAL_IP)
if CONF_NETWORKS in config:
raise vol.Invalid("You cannot use the 'ssid:' option together with 'networks:'. Please "
"copy your network into the 'networks:' key")
config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network)
if (CONF_NETWORKS not in config) and (CONF_AP not in config):
raise vol.Invalid("Please specify at least an SSID or an Access Point " raise vol.Invalid("Please specify at least an SSID or an Access Point "
"to create.") "to create.")
return config return config
IPAddress = global_ns.class_('IPAddress')
ManualIP = esphomelib_ns.struct('ManualIP')
WiFiComponent = esphomelib_ns.class_('WiFiComponent', Component)
WiFiAp = esphomelib_ns.struct('WiFiAp')
WiFiPowerSaveMode = esphomelib_ns.enum('WiFiPowerSaveMode')
WIFI_POWER_SAVE_MODES = {
'NONE': WiFiPowerSaveMode.WIFI_POWER_SAVE_NONE,
'LIGHT': WiFiPowerSaveMode.WIFI_POWER_SAVE_LIGHT,
'HIGH': WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH,
}
CONFIG_SCHEMA = vol.All(vol.Schema({ CONFIG_SCHEMA = vol.All(vol.Schema({
cv.GenerateID(): cv.declare_variable_id(WiFiComponent), cv.GenerateID(): cv.declare_variable_id(WiFiComponent),
vol.Optional(CONF_NETWORKS): cv.ensure_list(WIFI_NETWORK_STA),
vol.Optional(CONF_SSID): cv.ssid, vol.Optional(CONF_SSID): cv.ssid,
vol.Optional(CONF_PASSWORD): validate_password, vol.Optional(CONF_PASSWORD): validate_password,
vol.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, vol.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA,
vol.Optional(CONF_AP): WIFI_NETWORK_AP, vol.Optional(CONF_AP): WIFI_NETWORK_AP,
vol.Optional(CONF_HOSTNAME): cv.hostname, vol.Optional(CONF_HOSTNAME): cv.hostname,
vol.Optional(CONF_DOMAIN, default='.local'): cv.domain_name, vol.Optional(CONF_DOMAIN, default='.local'): cv.domain_name,
@ -110,21 +128,28 @@ def manual_ip(config):
def wifi_network(config): def wifi_network(config):
return StructInitializer( ap = variable(config[CONF_ID], WiFiAP())
WiFiAp, if CONF_SSID in config:
('ssid', config.get(CONF_SSID, "")), add(ap.set_ssid(config[CONF_SSID]))
('password', config.get(CONF_PASSWORD, "")), if CONF_PASSWORD in config:
('channel', config.get(CONF_CHANNEL, -1)), add(ap.set_password(config[CONF_PASSWORD]))
('manual_ip', manual_ip(config.get(CONF_MANUAL_IP))), if CONF_BSSID in config:
) bssid = [HexInt(i) for i in config[CONF_BSSID].parts]
add(ap.set_bssid(ArrayInitializer(*bssid, multiline=False)))
if CONF_CHANNEL in config:
add(ap.set_channel(config[CONF_CHANNEL]))
if CONF_MANUAL_IP in config:
add(ap.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))
return ap
def to_code(config): def to_code(config):
rhs = App.init_wifi() rhs = App.init_wifi()
wifi = Pvariable(config[CONF_ID], rhs) wifi = Pvariable(config[CONF_ID], rhs)
if CONF_SSID in config: for network in config.get(CONF_NETWORKS, []):
add(wifi.set_sta(wifi_network(config))) add(wifi.add_sta(wifi_network(network)))
if CONF_AP in config: if CONF_AP in config:
add(wifi.set_ap(wifi_network(config[CONF_AP]))) add(wifi.set_ap(wifi_network(config[CONF_AP])))

View File

@ -102,13 +102,25 @@ def boolean(value):
return bool(value) return bool(value)
def ensure_list(value): def ensure_list(*validators):
"""Wrap value in list if it is not one.""" """Wrap value in list if it is not one."""
if value is None or (isinstance(value, dict) and not value): user = vol.All(*validators)
return []
if isinstance(value, list): def validator(value):
return value if value is None or (isinstance(value, dict) and not value):
return [value] return []
if not isinstance(value, list):
return [user(value)]
ret = []
for i, val in enumerate(value):
try:
ret.append(user(val))
except vol.Invalid as err:
err.prepend(i)
raise err
return ret
return validator
def ensure_list_not_empty(value): def ensure_list_not_empty(value):
@ -469,16 +481,18 @@ def ssid(value):
raise vol.Invalid("SSID must be a string. Did you wrap it in quotes?") raise vol.Invalid("SSID must be a string. Did you wrap it in quotes?")
if not value: if not value:
raise vol.Invalid("SSID can't be empty.") raise vol.Invalid("SSID can't be empty.")
if len(value) > 31: if len(value) > 32:
raise vol.Invalid("SSID can't be longer than 31 characters") raise vol.Invalid("SSID can't be longer than 32 characters")
return value return value
def ipv4(value): def ipv4(value):
if isinstance(value, list): if isinstance(value, list):
parts = value parts = value
elif isinstance(value, str): elif isinstance(value, basestring):
parts = value.split('.') parts = value.split('.')
elif isinstance(value, IPAddress):
return value
else: else:
raise vol.Invalid("IPv4 address must consist of either string or " raise vol.Invalid("IPv4 address must consist of either string or "
"integer list") "integer list")
@ -664,6 +678,16 @@ def file_(value):
return value return value
ENTITY_ID_PATTERN = re.compile(r"^([a-z0-9]+)\.([a-z0-9]+)$")
def entity_id(value):
value = string_strict(value).lower()
if ENTITY_ID_PATTERN.match(value) is None:
raise vol.Invalid(u"Invalid entity ID: {}".format(value))
return value
class GenerateID(vol.Optional): class GenerateID(vol.Optional):
def __init__(self, key=CONF_ID): def __init__(self, key=CONF_ID):
super(GenerateID, self).__init__(key, default=lambda: None) super(GenerateID, self).__init__(key, default=lambda: None)

View File

@ -28,6 +28,7 @@ CONF_BRANCH = 'branch'
CONF_LOGGER = 'logger' CONF_LOGGER = 'logger'
CONF_WIFI = 'wifi' CONF_WIFI = 'wifi'
CONF_SSID = 'ssid' CONF_SSID = 'ssid'
CONF_BSSID = 'bssid'
CONF_PASSWORD = 'password' CONF_PASSWORD = 'password'
CONF_MANUAL_IP = 'manual_ip' CONF_MANUAL_IP = 'manual_ip'
CONF_STATIC_IP = 'static_ip' CONF_STATIC_IP = 'static_ip'
@ -222,6 +223,7 @@ CONF_ACCURACY = 'accuracy'
CONF_BOARD_FLASH_MODE = 'board_flash_mode' CONF_BOARD_FLASH_MODE = 'board_flash_mode'
CONF_ON_PRESS = 'on_press' CONF_ON_PRESS = 'on_press'
CONF_ON_RELEASE = 'on_release' CONF_ON_RELEASE = 'on_release'
CONF_ON_STATE = 'on_state'
CONF_ON_CLICK = 'on_click' CONF_ON_CLICK = 'on_click'
CONF_ON_DOUBLE_CLICK = 'on_double_click' CONF_ON_DOUBLE_CLICK = 'on_double_click'
CONF_ON_MULTI_CLICK = 'on_multi_click' CONF_ON_MULTI_CLICK = 'on_multi_click'
@ -386,6 +388,10 @@ CONF_PIN_D = 'pin_d'
CONF_SLEEP_WHEN_DONE = 'sleep_when_done' CONF_SLEEP_WHEN_DONE = 'sleep_when_done'
CONF_STEP_MODE = 'step_mode' CONF_STEP_MODE = 'step_mode'
CONF_COMPONENTS = 'components' CONF_COMPONENTS = 'components'
CONF_DATA_TEMPLATE = 'data_template'
CONF_VARIABLES = 'variables'
CONF_SERVICE = 'service'
CONF_ENTITY_ID = 'entity_id'
ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_' ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_'
ARDUINO_VERSION_ESP32_DEV = 'https://github.com/platformio/platform-espressif32.git#feature/stage' ARDUINO_VERSION_ESP32_DEV = 'https://github.com/platformio/platform-espressif32.git#feature/stage'

View File

@ -282,6 +282,8 @@ class ID(object):
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
class EsphomeyamlCore(object): class EsphomeyamlCore(object):
def __init__(self): def __init__(self):
# True if command is run from dashboard
self.dashboard = False
# The name of the node # The name of the node
self.name = None # type: str self.name = None # type: str
# The relative path to the configuration YAML # The relative path to the configuration YAML

View File

@ -175,8 +175,8 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_ON_LOOP): automation.validate_automation({ vol.Optional(CONF_ON_LOOP): automation.validate_automation({
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(LoopTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(LoopTrigger),
}), }),
vol.Optional(CONF_INCLUDES): vol.All(cv.ensure_list, [cv.file_]), vol.Optional(CONF_INCLUDES): cv.ensure_list(cv.file_),
vol.Optional(CONF_LIBRARIES): vol.All(cv.ensure_list, [cv.string_strict]), vol.Optional(CONF_LIBRARIES): cv.ensure_list(cv.string_strict),
vol.Optional('library_uri'): cv.invalid("The library_uri option has been removed in 1.8.0 and " vol.Optional('library_uri'): cv.invalid("The library_uri option has been removed in 1.8.0 and "
"was moved into the esphomelib_version option."), "was moved into the esphomelib_version option."),

View File

@ -103,49 +103,49 @@ class EsphomeyamlLogsHandler(EsphomeyamlCommandWebSocket):
def build_command(self, message): def build_command(self, message):
js = json.loads(message) js = json.loads(message)
config_file = CONFIG_DIR + '/' + js['configuration'] config_file = CONFIG_DIR + '/' + js['configuration']
return ["esphomeyaml", config_file, "logs", '--serial-port', js["port"]] return ["esphomeyaml", "--dashboard", config_file, "logs", '--serial-port', js["port"]]
class EsphomeyamlRunHandler(EsphomeyamlCommandWebSocket): class EsphomeyamlRunHandler(EsphomeyamlCommandWebSocket):
def build_command(self, message): def build_command(self, message):
js = json.loads(message) js = json.loads(message)
config_file = os.path.join(CONFIG_DIR, js['configuration']) config_file = os.path.join(CONFIG_DIR, js['configuration'])
return ["esphomeyaml", config_file, "run", '--upload-port', js["port"]] return ["esphomeyaml", "--dashboard", config_file, "run", '--upload-port', js["port"]]
class EsphomeyamlCompileHandler(EsphomeyamlCommandWebSocket): class EsphomeyamlCompileHandler(EsphomeyamlCommandWebSocket):
def build_command(self, message): def build_command(self, message):
js = json.loads(message) js = json.loads(message)
config_file = os.path.join(CONFIG_DIR, js['configuration']) config_file = os.path.join(CONFIG_DIR, js['configuration'])
return ["esphomeyaml", config_file, "compile"] return ["esphomeyaml", "--dashboard", config_file, "compile"]
class EsphomeyamlValidateHandler(EsphomeyamlCommandWebSocket): class EsphomeyamlValidateHandler(EsphomeyamlCommandWebSocket):
def build_command(self, message): def build_command(self, message):
js = json.loads(message) js = json.loads(message)
config_file = os.path.join(CONFIG_DIR, js['configuration']) config_file = os.path.join(CONFIG_DIR, js['configuration'])
return ["esphomeyaml", config_file, "config"] return ["esphomeyaml", "--dashboard", config_file, "config"]
class EsphomeyamlCleanMqttHandler(EsphomeyamlCommandWebSocket): class EsphomeyamlCleanMqttHandler(EsphomeyamlCommandWebSocket):
def build_command(self, message): def build_command(self, message):
js = json.loads(message) js = json.loads(message)
config_file = os.path.join(CONFIG_DIR, js['configuration']) config_file = os.path.join(CONFIG_DIR, js['configuration'])
return ["esphomeyaml", config_file, "clean-mqtt"] return ["esphomeyaml", "--dashboard", config_file, "clean-mqtt"]
class EsphomeyamlCleanHandler(EsphomeyamlCommandWebSocket): class EsphomeyamlCleanHandler(EsphomeyamlCommandWebSocket):
def build_command(self, message): def build_command(self, message):
js = json.loads(message) js = json.loads(message)
config_file = os.path.join(CONFIG_DIR, js['configuration']) config_file = os.path.join(CONFIG_DIR, js['configuration'])
return ["esphomeyaml", config_file, "clean"] return ["esphomeyaml", "--dashboard", config_file, "clean"]
class EsphomeyamlHassConfigHandler(EsphomeyamlCommandWebSocket): class EsphomeyamlHassConfigHandler(EsphomeyamlCommandWebSocket):
def build_command(self, message): def build_command(self, message):
js = json.loads(message) js = json.loads(message)
config_file = os.path.join(CONFIG_DIR, js['configuration']) config_file = os.path.join(CONFIG_DIR, js['configuration'])
return ["esphomeyaml", config_file, "hass-config"] return ["esphomeyaml", "--dashboard", config_file, "hass-config"]
class SerialPortRequestHandler(BaseHandler): class SerialPortRequestHandler(BaseHandler):
@ -294,10 +294,9 @@ class MainRequestHandler(BaseHandler):
version = const.__version__ version = const.__version__
docs_link = 'https://beta.esphomelib.com/esphomeyaml/' if 'b' in version else \ docs_link = 'https://beta.esphomelib.com/esphomeyaml/' if 'b' in version else \
'https://esphomelib.com/esphomeyaml/' 'https://esphomelib.com/esphomeyaml/'
mqtt_config = get_mqtt_config_lazy()
self.render("templates/index.html", entries=entries, self.render("templates/index.html", entries=entries,
version=version, begin=begin, docs_link=docs_link, mqtt_config=mqtt_config) version=version, begin=begin, docs_link=docs_link)
def _ping_func(filename, address): def _ping_func(filename, address):
@ -497,43 +496,6 @@ def make_app(debug=False):
return app return app
def _get_mqtt_config_impl():
import requests
headers = {
'X-HASSIO-KEY': os.getenv('HASSIO_TOKEN'),
}
mqtt_config = requests.get('http://hassio/services/mqtt', headers=headers).json()['data']
info = requests.get('http://hassio/host/info', headers=headers).json()['data']
host = '{}.local'.format(info['hostname'])
port = mqtt_config['port']
if port != 1883:
host = '{}:{}'.format(host, port)
return {
'ssl': mqtt_config['ssl'],
'host': host,
'username': mqtt_config.get('username', ''),
'password': mqtt_config.get('password', '')
}
def get_mqtt_config_lazy():
global HASSIO_MQTT_CONFIG
if not ON_HASSIO:
return None
if HASSIO_MQTT_CONFIG is None:
try:
HASSIO_MQTT_CONFIG = _get_mqtt_config_impl()
except Exception: # pylint: disable=broad-except
pass
return HASSIO_MQTT_CONFIG
def start_web_server(args): def start_web_server(args):
global CONFIG_DIR global CONFIG_DIR
global PASSWORD_DIGEST global PASSWORD_DIGEST

View File

@ -15,7 +15,7 @@ const initializeColorState = () => {
}; };
const colorReplace = (pre, state, text) => { const colorReplace = (pre, state, text) => {
const re = /\033(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g; const re = /(?:\033|\\033)(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
let i = 0; let i = 0;
if (state.carriageReturn) { if (state.carriageReturn) {
@ -176,7 +176,7 @@ const fetchPing = () => {
fetch('/ping', {credentials: "same-origin"}).then(res => res.json()) fetch('/ping', {credentials: "same-origin"}).then(res => res.json())
.then(response => { .then(response => {
for (let filename of response) { for (let filename in response) {
let node = document.querySelector(`.status-indicator[data-node="${filename}"]`); let node = document.querySelector(`.status-indicator[data-node="${filename}"]`);
if (node === null) if (node === null)
continue; continue;

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>esphomeyaml Dashboard</title> <title>ESPHome Dashboard</title>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="/static/materialize.min.css?v=1"> <link rel="stylesheet" href="/static/materialize.min.css?v=1">
<link rel="stylesheet" href="/static/materialize-stepper.min.css?v=1"> <link rel="stylesheet" href="/static/materialize-stepper.min.css?v=1">
@ -22,7 +22,7 @@
<header> <header>
<nav> <nav>
<div class="nav-wrapper indigo"> <div class="nav-wrapper indigo">
<a href="#" class="brand-logo left">esphomeyaml Dashboard</a> <a href="#" class="brand-logo left">ESPHome Dashboard</a>
<div class="select-port-container right" id="select-port-target"> <div class="select-port-container right" id="select-port-target">
<select></select> <select></select>
</div> </div>
@ -33,7 +33,7 @@
<div class="tap-target-content"> <div class="tap-target-content">
<h5>Select Upload Port</h5> <h5>Select Upload Port</h5>
<p> <p>
Here you can select where esphomeyaml will attempt to show logs and upload firmwares to. Here you can select where ESPHome will attempt to show logs and upload firmwares to.
By default, this is "OTA", or Over-The-Air. Note that you might have to restart the Hass.io add-on By default, this is "OTA", or Over-The-Air. Note that you might have to restart the Hass.io add-on
for new serial ports to be detected. for new serial ports to be detected.
</p> </p>
@ -81,7 +81,7 @@
<li><a class="action-clean-mqtt" data-node="{{ entry.filename }}">Clean MQTT</a></li> <li><a class="action-clean-mqtt" data-node="{{ entry.filename }}">Clean MQTT</a></li>
<li><a class="action-clean" data-node="{{ entry.filename }}">Clean Build</a></li> <li><a class="action-clean" data-node="{{ entry.filename }}">Clean Build</a></li>
<li><a class="action-compile" data-node="{{ entry.filename }}">Compile</a></li> <li><a class="action-compile" data-node="{{ entry.filename }}">Compile</a></li>
<li><a class="action-hass-config" data-node="{{ entry.filename }}">Home Assistant Configuration</a></li> <li><a class="action-hass-config" data-node="{{ entry.filename }}">HASS MQTT Configuration</a></li>
</ul> </ul>
</div> </div>
</div> </div>
@ -164,8 +164,8 @@
<div class="step-content"> <div class="step-content">
<div class="row"> <div class="row">
<p> <p>
Hi there! I'm the esphomeyaml setup wizard and will guide you through setting up Hi there! I'm the ESPHome setup wizard and will guide you through setting up
your first ESP8266 or ESP32-powered device using esphomeyaml. your first ESP8266 or ESP32-powered device using ESPHome.
</p> </p>
<a href="https://www.espressif.com/en/products/hardware/esp8266ex/overview" target="_blank">ESP8266s</a> and <a href="https://www.espressif.com/en/products/hardware/esp8266ex/overview" target="_blank">ESP8266s</a> and
their successors (the <a href="https://www.espressif.com/en/products/hardware/esp32/overview" target="_blank">ESP32s</a>) their successors (the <a href="https://www.espressif.com/en/products/hardware/esp32/overview" target="_blank">ESP32s</a>)
@ -174,19 +174,19 @@
such as the <a href="https://esphomelib.com/esphomeyaml/devices/nodemcu_esp8266.html" target="_blank">NodeMCU</a>. such as the <a href="https://esphomelib.com/esphomeyaml/devices/nodemcu_esp8266.html" target="_blank">NodeMCU</a>.
<p> <p>
</p> </p>
<a href="https://esphomelib.com/esphomeyaml/index.html" target="_blank">esphomeyaml</a>, <a href="https://esphomelib.com/esphomeyaml/index.html" target="_blank">ESPHome</a>,
the tool you're using here, creates custom firmwares for these devices using YAML configuration the tool you're using here, creates custom firmwares for these devices using YAML configuration
files (similar to the ones you might be used to with Home Assistant). files (similar to the ones you might be used to with Home Assistant).
<p> <p>
</p> </p>
This wizard will create a basic YAML configuration file for your "node" (the microcontroller). This wizard will create a basic YAML configuration file for your "node" (the microcontroller).
Later, you will be able to customize this file and add some of Later, you will be able to customize this file and add some of ESPHome's
<a href="https://github.com/OttoWinter/esphomelib" target="_blank">esphomelib's</a>
many integrations. many integrations.
<p> <p>
<p> <p>
First, I need to know what this node should be called. Choose this name wisely, changing this First, I need to know what this node should be called. Choose this name wisely, it should be unique among
later makes Over-The-Air Update attempts difficult. all your ESPs.
Names must be <strong>lowercase</strong> and <strong>must not contain spaces</strong> (allowed characters: <code class="inlinecode">a-z</code>, Names must be <strong>lowercase</strong> and <strong>must not contain spaces</strong> (allowed characters: <code class="inlinecode">a-z</code>,
<code class="inlinecode">0-9</code> and <code class="inlinecode">_</code>) <code class="inlinecode">0-9</code> and <code class="inlinecode">_</code>)
</p> </p>
@ -321,73 +321,15 @@
<label for="wifi_password">WiFi Password</label> <label for="wifi_password">WiFi Password</label>
</div> </div>
<p> <p>
Esphomelib automatically sets up an Over-The-Air update server on the node ESPHome automatically sets up an Over-The-Air update server on the node
so that you only need to flash a firmware via USB once. so that you only need to flash a firmware via USB once. This password
is also used to connect to the ESP from Home Assistant.
Optionally, you can set a password for this upload process here: Optionally, you can set a password for this upload process here:
</p> </p>
<div class="input-field col s12"> <div class="input-field col s12">
<input id="ota_password" class="validate" name="ota_password" type="password"> <input id="password" class="validate" name="password" type="password">
<label for="ota_password">OTA Password</label> <label for="password">Access Password</label>
</div>
</div>
<div class="step-actions">
<button class="waves-effect waves-dark btn indigo next-step">CONTINUE</button>
</div>
</div>
</li>
<li class="step">
<div class="step-title waves-effect">MQTT</div>
<div class="step-content">
<div class="row">
{% if mqtt_config is None %}
<p>
esphomelib connects to your Home Assistant instance via
<a href="https://www.home-assistant.io/docs/mqtt/">MQTT</a>.
If you haven't already, please set up
MQTT on your Home Assistant server, for example with the
<a href="https://www.home-assistant.io/addons/mosquitto/">Mosquitto Hass.io Add-on</a>.
</p>
<p>
When you're done with that, please enter your MQTT broker here. For example
<code class="inlinecode">192.168.1.100</code>.
Please also specify the MQTT username and password you wish esphomelib to use
(leave them empty if you're not using any authentication).
</p>
{% else %}
<p>
esphomelib connects to your Home Assistant instance via
<a href="https://www.home-assistant.io/docs/mqtt/">MQTT</a>. In this section you will have
to tell esphomelib which MQTT "broker" to use.
</p>
<p>
It looks like you've already set up MQTT, the values below are taken from your Hass.io MQTT add-on.
Please confirm they are correct and press CONTINUE.
</p>
{% end %}
<div class="input-field col s12">
{% if mqtt_config is None %}
<input id="mqtt_broker" class="validate" type="text" name="broker" required>
{% else %}
<input id="mqtt_broker" class="validate" type="text" name="broker" value="{{ mqtt_config['host'] }}" required>
{% end %}
<label for="mqtt_broker">MQTT Broker</label>
</div>
<div class="input-field col s6">
{% if mqtt_config is None %}
<input id="mqtt_username" class="validate" type="text" name="mqtt_username">
{% else %}
<input id="mqtt_username" class="validate" type="text" name="mqtt_username" value="{{ mqtt_config['username'] }}">
{% end%}
<label for="mqtt_username">MQTT Username</label>
</div>
<div class="input-field col s6">
{% if mqtt_config is None %}
<input id="mqtt_password" class="validate" name="mqtt_password" type="password">
{% else %}
<input id="mqtt_password" class="validate" name="mqtt_password" type="password" value="{{ mqtt_config['password'] }}">
{% end %}
<label for="mqtt_password">MQTT Password</label>
</div> </div>
</div> </div>
<div class="step-actions"> <div class="step-actions">
@ -399,7 +341,7 @@
<div class="step-title waves-effect">Done!</div> <div class="step-title waves-effect">Done!</div>
<div class="step-content"> <div class="step-content">
<p> <p>
Hooray! 🎉🎉🎉 You've successfully created your first esphomeyaml configuration file. Hooray! 🎉🎉🎉 You've successfully created your first ESPHome configuration file.
When you click Submit, I will save this configuration file under When you click Submit, I will save this configuration file under
<code class="inlinecode">&lt;HASS_CONFIG_FOLDER&gt;/esphomeyaml/&lt;NAME_OF_NODE&gt;.yaml</code> and <code class="inlinecode">&lt;HASS_CONFIG_FOLDER&gt;/esphomeyaml/&lt;NAME_OF_NODE&gt;.yaml</code> and
you will be able to edit this file with the you will be able to edit this file with the
@ -421,7 +363,7 @@
</a>. </a>.
</li> </li>
<li> <li>
See the <a href="https://esphomelib.com/esphomeyaml/index.html" target="_blank">esphomeyaml index</a> See the <a href="https://esphomelib.com/esphomeyaml/index.html" target="_blank">ESPHome index</a>
for a list of supported sensors/devices. for a list of supported sensors/devices.
</li> </li>
<li> <li>
@ -429,8 +371,8 @@
have time, I would be happy to help with issues and discuss new features. have time, I would be happy to help with issues and discuss new features.
</li> </li>
<li> <li>
Star <a href="https://github.com/OttoWinter/esphomelib" target="_blank">esphomelib</a> and Star <a href="https://github.com/OttoWinter/esphomelib" target="_blank">ESPHome Core</a> and
<a href="https://github.com/OttoWinter/esphomeyaml" target="_blank">esphomeyaml</a> on GitHub <a href="https://github.com/OttoWinter/esphomeyaml" target="_blank">ESPHome</a> on GitHub
if you find this software awesome and report issues using the bug trackers there. if you find this software awesome and report issues using the bug trackers there.
</li> </li>
</ul> </ul>
@ -508,7 +450,7 @@
<div class="tap-target-content"> <div class="tap-target-content">
<h5>Set up your first Node</h5> <h5>Set up your first Node</h5>
<p> <p>
Huh... It seems like you you don't have any esphomeyaml configuration files yet... Huh... It seems like you you don't have any ESPHome configuration files yet...
Fortunately, there's a setup wizard that will step you through setting up your first node 🎉 Fortunately, there's a setup wizard that will step you through setting up your first node 🎉
</p> </p>
</div> </div>
@ -522,7 +464,7 @@
<div class="footer-copyright"> <div class="footer-copyright">
<div class="container"> <div class="container">
© 2018 Copyright Otto Winter, Made with <a class="grey-text text-lighten-4" href="https://materializecss.com/" target="_blank">Materialize</a> © 2018 Copyright Otto Winter, Made with <a class="grey-text text-lighten-4" href="https://materializecss.com/" target="_blank">Materialize</a>
<a class="grey-text text-lighten-4 right" href="{{ docs_link }}" target="_blank">esphomeyaml {{ version }} Documentation</a> <a class="grey-text text-lighten-4 right" href="{{ docs_link }}" target="_blank">ESPHome {{ version }} Documentation</a>
</div> </div>
</div> </div>
</footer> </footer>

View File

@ -3,8 +3,10 @@ import logging
import random import random
import socket import socket
import sys import sys
import time
from esphomeyaml.core import EsphomeyamlError from esphomeyaml.core import EsphomeyamlError
from esphomeyaml.helpers import resolve_ip_address, is_ip_address
RESPONSE_OK = 0 RESPONSE_OK = 0
RESPONSE_REQUEST_AUTH = 1 RESPONSE_REQUEST_AUTH = 1
@ -221,50 +223,26 @@ def perform_ota(sock, password, file_handle, filename):
_LOGGER.info("OTA successful") _LOGGER.info("OTA successful")
# Do not connect logs until it is fully on
def is_ip_address(host): time.sleep(2)
parts = host.split('.')
if len(parts) != 4:
return False
try:
for p in parts:
int(p)
return True
except ValueError:
return False
def resolve_ip_address(host):
if is_ip_address(host):
_LOGGER.info("Connecting to %s", host)
return host
_LOGGER.info("Resolving IP Address of %s", host)
hosts = [host]
if host.endswith('.local'):
hosts.append(host[:-6])
errors = []
for x in hosts:
try:
ip = socket.gethostbyname(x)
break
except socket.error as err:
errors.append(err)
else:
_LOGGER.error("Error resolving IP address of %s. Is it connected to WiFi?",
host)
_LOGGER.error("(If this error persists, please set a static IP address: "
"https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips)")
raise OTAError("Errors: {}".format(', '.join(str(x) for x in errors)))
_LOGGER.info(" -> %s", ip)
return ip
def run_ota_impl_(remote_host, remote_port, password, filename): def run_ota_impl_(remote_host, remote_port, password, filename):
ip = resolve_ip_address(remote_host) if is_ip_address(remote_host):
_LOGGER.info("Connecting to %s", remote_host)
ip = remote_host
else:
_LOGGER.info("Resolving IP address of %s", remote_host)
try:
ip = resolve_ip_address(remote_host)
except EsphomeyamlError as err:
_LOGGER.error("Error resolving IP address of %s. Is it connected to WiFi?",
remote_host)
_LOGGER.error("(If this error persists, please set a static IP address: "
"https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips)")
raise OTAError(err)
_LOGGER.info(" -> %s", ip)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(10.0) sock.settimeout(10.0)
try: try:

View File

@ -3,6 +3,7 @@ from __future__ import print_function
import errno import errno
import logging import logging
import os import os
import socket
import subprocess import subprocess
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -75,3 +76,26 @@ def mkdir_p(path):
pass pass
else: else:
raise raise
def is_ip_address(host):
parts = host.split('.')
if len(parts) != 4:
return False
try:
for p in parts:
int(p)
return True
except ValueError:
return False
def resolve_ip_address(host):
try:
ip = socket.gethostbyname(host)
except socket.error as err:
from esphomeyaml.core import EsphomeyamlError
raise EsphomeyamlError("Error resolving IP address: {}".format(err))
return ip

View File

@ -27,6 +27,14 @@ class ServiceRegistry(dict):
def safe_print(message=""): def safe_print(message=""):
from esphomeyaml.core import CORE
if CORE.dashboard:
try:
message = message.replace('\033', '\\033')
except UnicodeEncodeError:
pass
try: try:
print(message) print(message)
return return
@ -48,6 +56,29 @@ def shlex_quote(s):
return u"'" + s.replace(u"'", u"'\"'\"'") + u"'" return u"'" + s.replace(u"'", u"'\"'\"'") + u"'"
class RedirectText(object):
def __init__(self, out):
self._out = out
def __getattr__(self, item):
return getattr(self._out, item)
def write(self, s):
from esphomeyaml.core import CORE
if CORE.dashboard:
try:
s = s.replace('\033', '\\033')
except UnicodeEncodeError:
pass
self._out.write(s)
# pylint: disable=no-self-use
def isatty(self):
return True
def run_external_command(func, *cmd, **kwargs): def run_external_command(func, *cmd, **kwargs):
def mock_exit(return_code): def mock_exit(return_code):
raise SystemExit(return_code) raise SystemExit(return_code)
@ -57,6 +88,9 @@ def run_external_command(func, *cmd, **kwargs):
full_cmd = u' '.join(shlex_quote(x) for x in cmd) full_cmd = u' '.join(shlex_quote(x) for x in cmd)
_LOGGER.info(u"Running: %s", full_cmd) _LOGGER.info(u"Running: %s", full_cmd)
sys.stdout = RedirectText(sys.stdout)
sys.stderr = RedirectText(sys.stderr)
capture_stdout = kwargs.get('capture_stdout', False) capture_stdout = kwargs.get('capture_stdout', False)
if capture_stdout: if capture_stdout:
sys.stdout = io.BytesIO() sys.stdout = io.BytesIO()
@ -76,6 +110,11 @@ def run_external_command(func, *cmd, **kwargs):
sys.argv = orig_argv sys.argv = orig_argv
sys.exit = orig_exit sys.exit = orig_exit
if isinstance(sys.stdout, RedirectText):
sys.stdout = sys.__stdout__
if isinstance(sys.stderr, RedirectText):
sys.stderr = sys.__stderr__
if capture_stdout: if capture_stdout:
# pylint: disable=lost-exception # pylint: disable=lost-exception
stdout = sys.stdout.getvalue() stdout = sys.stdout.getvalue()

View File

@ -35,13 +35,6 @@ WIFI_BIG = """ __ ___ ______ _
\ /\ / | | | | | \ /\ / | | | | |
\/ \/ |_|_| |_| \/ \/ |_|_| |_|
""" """
MQTT_BIG = """ __ __ ____ _______ _______
| \/ |/ __ \__ __|__ __|
| \ / | | | | | | | |
| |\/| | | | | | | | |
| | | | |__| | | | | |
|_| |_|\___\_\ |_| |_|
"""
OTA_BIG = """ ____ _______ OTA_BIG = """ ____ _______
/ __ \__ __|/\\ / __ \__ __|/\\
| | | | | | / \\ | | | | | | / \\
@ -50,7 +43,6 @@ OTA_BIG = """ ____ _______
\____/ |_/_/ \_\\ \____/ |_/_/ \_\\
""" """
# TODO handle escaping
BASE_CONFIG = u"""esphomeyaml: BASE_CONFIG = u"""esphomeyaml:
name: {name} name: {name}
platform: {platform} platform: {platform}
@ -60,24 +52,21 @@ wifi:
ssid: '{ssid}' ssid: '{ssid}'
password: '{psk}' password: '{psk}'
mqtt:
broker: '{broker}'
username: '{mqtt_username}'
password: '{mqtt_password}'
# Enable logging # Enable logging
logger: logger:
# Enable Home Assistant API
api:
""" """
def wizard_file(**kwargs): def wizard_file(**kwargs):
config = BASE_CONFIG.format(**kwargs) config = BASE_CONFIG.format(**kwargs)
if kwargs['ota_password']: if kwargs['password']:
config += u"ota:\n password: '{}'\n".format(kwargs['ota_password']) config += u" password: '{0}'\n\nota:\n password: '{0}'\n".format(kwargs['password'])
else: else:
config += u"ota:\n" config += u"\nota:\n"
return config return config
@ -135,11 +124,11 @@ def wizard(path):
return 1 return 1
safe_print("Hi there!") safe_print("Hi there!")
sleep(1.5) sleep(1.5)
safe_print("I'm the wizard of esphomeyaml :)") safe_print("I'm the wizard of ESPHome :)")
sleep(1.25) sleep(1.25)
safe_print("And I'm here to help you get started with esphomeyaml.") safe_print("And I'm here to help you get started with ESPHome.")
sleep(2.0) sleep(2.0)
safe_print("In 5 steps I'm going to guide you through creating a basic " safe_print("In 4 steps I'm going to guide you through creating a basic "
"configuration file for your custom ESP8266/ESP32 firmware. Yay!") "configuration file for your custom ESP8266/ESP32 firmware. Yay!")
sleep(3.0) sleep(3.0)
safe_print() safe_print()
@ -205,6 +194,8 @@ def wizard(path):
else: else:
safe_print("For example \"{}\".".format(color("bold_white", 'nodemcuv2'))) safe_print("For example \"{}\".".format(color("bold_white", 'nodemcuv2')))
boards = list(ESP8266_BOARD_PINS.keys()) boards = list(ESP8266_BOARD_PINS.keys())
safe_print("Options: {}".format(', '.join(boards)))
while True: while True:
board = raw_input(color("bold_white", "(board): ")) board = raw_input(color("bold_white", "(board): "))
try: try:
@ -214,7 +205,6 @@ def wizard(path):
safe_print(color('red', "Sorry, I don't think the board \"{}\" exists.")) safe_print(color('red', "Sorry, I don't think the board \"{}\" exists."))
safe_print() safe_print()
sleep(0.25) sleep(0.25)
safe_print("Possible options are {}".format(', '.join(boards)))
safe_print() safe_print()
safe_print(u"Way to go! You've chosen {} as your board.".format(color('cyan', board))) safe_print(u"Way to go! You've chosen {} as your board.".format(color('cyan', board)))
@ -255,60 +245,26 @@ def wizard(path):
safe_print("Perfect! WiFi is now set up (you can create static IPs and so on later).") safe_print("Perfect! WiFi is now set up (you can create static IPs and so on later).")
sleep(1.5) sleep(1.5)
safe_print_step(4, MQTT_BIG) safe_print_step(4, OTA_BIG)
safe_print("Almost there! Now let's setup MQTT so that your node can connect to the " safe_print("Almost there! ESPHome can automatically upload custom firmwares over WiFi "
"outside world.") "(over the air) and integrates into Home Assistant with a native API.")
safe_print()
sleep(1)
safe_print("Please enter the " + color('green', 'address') + " of your MQTT broker.")
safe_print()
safe_print("For example \"{}\".".format(color('bold_white', '192.168.178.84')))
broker = raw_input(color('bold_white', "(broker): "))
safe_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)
safe_print(u"MQTT Username: \"{}\"; Password: \"{}\""
u"".format(color('cyan', mqtt_username), color('cyan', show)))
else:
safe_print("No authentication for MQTT")
safe_print_step(5, OTA_BIG)
safe_print("Last step! esphomeyaml can automatically upload custom firmwares over WiFi "
"(over the air).")
safe_print("This can be insecure if you do not trust the WiFi network. Do you want to set " safe_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?") "a " + color('green', 'password') + " for connecting to this ESP?")
safe_print() safe_print()
sleep(0.25) sleep(0.25)
safe_print("Press ENTER for no password") safe_print("Press ENTER for no password")
ota_password = raw_input(color('bold_white', '(password): ')) password = raw_input(color('bold_white', '(password): '))
wizard_write(path=path, name=name, platform=platform, board=board, wizard_write(path=path, name=name, platform=platform, board=board,
ssid=ssid, psk=psk, broker=broker, ssid=ssid, psk=psk, password=password)
mqtt_username=mqtt_username, mqtt_password=mqtt_password,
ota_password=ota_password)
safe_print() safe_print()
safe_print(color('cyan', "DONE! I've now written a new configuration file to ") + safe_print(color('cyan', "DONE! I've now written a new configuration file to ") +
color('bold_cyan', path)) color('bold_cyan', path))
safe_print() safe_print()
safe_print("Next steps:") safe_print("Next steps:")
safe_print(" > If you haven't already, enable MQTT discovery in Home Assistant:") safe_print(" > Check your Home Assistant \"integrations\" screen. If all goes well, you "
safe_print() "should see your ESP being discovered automatically.")
safe_print(color('bold_white', "# In your configuration.yaml"))
safe_print(color('bold_white', "mqtt:"))
safe_print(color('bold_white', u" broker: {}".format(broker)))
safe_print(color('bold_white', " # ..."))
safe_print(color('bold_white', " discovery: True"))
safe_print()
safe_print(" > Then follow the rest of the getting started guide:") safe_print(" > Then follow the rest of the getting started guide:")
safe_print(" > https://esphomelib.com/esphomeyaml/guides/getting_started_command_line.html") safe_print(" > https://esphomelib.com/esphomeyaml/guides/getting_started_command_line.html")
return 0 return 0

View File

@ -279,6 +279,7 @@ def gather_lib_deps():
# Manual fix for AsyncTCP # Manual fix for AsyncTCP
if CORE.config[CONF_ESPHOMEYAML].get(CONF_ARDUINO_VERSION) == ARDUINO_VERSION_ESP32_DEV: if CORE.config[CONF_ESPHOMEYAML].get(CONF_ARDUINO_VERSION) == ARDUINO_VERSION_ESP32_DEV:
lib_deps.add('https://github.com/me-no-dev/AsyncTCP.git#idf-update') lib_deps.add('https://github.com/me-no-dev/AsyncTCP.git#idf-update')
lib_deps.remove('AsyncTCP@1.0.1')
# avoid changing build flags order # avoid changing build flags order
return sorted(x for x in lib_deps if x) return sorted(x for x in lib_deps if x)
@ -376,6 +377,7 @@ def write_platformio_project():
f.write("app1, app, ota_1, 0x200000, 0x190000,\n") f.write("app1, app, ota_1, 0x200000, 0x190000,\n")
f.write("eeprom, data, 0x99, 0x390000, 0x001000,\n") f.write("eeprom, data, 0x99, 0x390000, 0x001000,\n")
f.write("spiffs, data, spiffs, 0x391000, 0x00F000\n") f.write("spiffs, data, spiffs, 0x391000, 0x00F000\n")
write_gitignore()
write_platformio_ini(content, platformio_ini) write_platformio_ini(content, platformio_ini)
@ -427,3 +429,23 @@ def clean_build():
continue continue
_LOGGER.info("Deleting %s", dir_path) _LOGGER.info("Deleting %s", dir_path)
shutil.rmtree(dir_path) shutil.rmtree(dir_path)
GITIGNORE_CONTENT = """# Gitignore settings for esphomeyaml
# This is an example and may include too much for your use-case.
# You can modify this file to suit your needs.
/.esphomeyaml/
**/.pioenvs/
**/.piolibdeps/
**/lib/
**/src/
**/platformio.ini
/secrets.yaml
"""
def write_gitignore():
path = CORE.relative_path('.gitignore')
if not os.path.isfile(path):
with open(path, 'w') as f:
f.write(GITIGNORE_CONTENT)

View File

@ -1,5 +1,6 @@
[MASTER] [MASTER]
reports=no reports=no
ignore=api_pb2.py
disable= disable=
missing-docstring, missing-docstring,

View File

@ -6,3 +6,4 @@ colorlog>=3.1.2
tornado>=5.0.0 tornado>=5.0.0
esptool>=2.3.1 esptool>=2.3.1
typing>=3.0.0 typing>=3.0.0
protobuf>=3.4

View File

@ -4,3 +4,4 @@ description-file = README.md
[flake8] [flake8]
max-line-length = 120 max-line-length = 120
builtins = unicode, long, raw_input builtins = unicode, long, raw_input
exclude = api_pb2.py

View File

@ -30,6 +30,7 @@ REQUIRES = [
'tornado>=5.0.0', 'tornado>=5.0.0',
'esptool>=2.3.1', 'esptool>=2.3.1',
'typing>=3.0.0', 'typing>=3.0.0',
'protobuf>=3.4',
] ]
CLASSIFIERS = [ CLASSIFIERS = [

10
tests/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
# Gitignore settings for esphomeyaml
# This is an example and may include too much for your use-case.
# You can modify this file to suit your needs.
/.esphomeyaml/
**/.pioenvs/
**/.piolibdeps/
**/lib/
**/src/
**/platformio.ini
/secrets.yaml