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
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.
- 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.
- 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.):**
@ -33,7 +35,7 @@ Please add the link to the documentation at https://esphomelib.com/esphomeyaml/i
**Problem-relevant YAML-configuration entries:**
```yaml
PASTE YAML FILE HERE
```
**Traceback (if applicable):**

View File

@ -7,16 +7,15 @@ about: Suggest an idea for this project
<!-- 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.
- 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.**
<!--
A clear and concise description of what the problem is.
-->
Ex. I'm always frustrated when [...]
**Is your feature request related to a problem/use-case? Please describe.**
<!-- A clear and concise description of what the problem is. -->
**Describe the solution you'd like**
A description of what you want to happen.
**Describe the solution you'd like:**
<!-- A description of what you want to happen. -->
**Additional context**
Add any other context about the feature request here.
**Additional context:**
<!-- 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:
- 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.
-->
DO NOT POST NEW INTEGRATION REQUESTS HERE!
**What new integration would you wish to have?**
<!-- A name/description of the new integration/board. -->
Please post all new integration requests in the esphomelib repository:
**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.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Additional context**
Thank you!

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 [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:
- [ ] The code change is tested and works locally.
- [ ] 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:
- [ ] Documentation added/updated in [esphomedocs](https://github.com/OttoWinter/esphomedocs).

View File

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

View File

@ -9,6 +9,7 @@ import random
import sys
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.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, \
@ -22,7 +23,7 @@ from esphomeyaml.util import run_external_command, safe_print
_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():
@ -202,6 +203,8 @@ def show_logs(config, args, port):
if port != 'OTA' and serial_port:
run_miniterm(config, port)
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)
@ -368,6 +371,8 @@ def parse_args(argv):
parser = argparse.ArgumentParser(prog='esphomeyaml')
parser.add_argument('-v', '--verbose', help="Enable verbose esphomeyaml logs.",
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.')
subparsers = parser.add_subparsers(help='Commands', dest='command')
@ -445,6 +450,7 @@ def parse_args(argv):
def run_esphomeyaml(argv):
args = parse_args(argv)
CORE.dashboard = args.dashboard
setup_log(args.verbose)
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):
is_list = isinstance(value, list)
value = cv.ensure_list(value)[:]
value = cv.ensure_list()(value)[:]
for i, item in enumerate(value):
path = [i] if is_list else []
item = copy.deepcopy(item)
@ -61,7 +61,8 @@ def validate_recursive_condition(value):
def validate_recursive_action(value):
is_list = isinstance(value, list)
value = cv.ensure_list(value)[:]
if not is_list:
value = [value]
for i, item in enumerate(value):
path = [i] if is_list else []
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_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_TIMING, CONF_TRIGGER_ID
CONF_TIMING, CONF_TRIGGER_ID, CONF_ON_STATE
from esphomeyaml.core import CORE
from esphomeyaml.cpp_generator import process_lambda, ArrayInitializer, add, Pvariable, \
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))
MultiClickTrigger = binary_sensor_ns.class_('MultiClickTrigger', Trigger.template(NoArg), Component)
MultiClickTriggerEvent = binary_sensor_ns.struct('MultiClickTriggerEvent')
StateTrigger = binary_sensor_ns.class_('StateTrigger', Trigger.template(bool_))
# 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]
FILTERS_SCHEMA = vol.All(cv.ensure_list, [vol.All({
FILTERS_SCHEMA = cv.ensure_list({
vol.Optional(CONF_INVERT): None,
vol.Optional(CONF_DELAYED_ON): cv.positive_time_period_milliseconds,
vol.Optional(CONF_DELAYED_OFF): cv.positive_time_period_milliseconds,
vol.Optional(CONF_HEARTBEAT): cv.positive_time_period_milliseconds,
vol.Optional(CONF_LAMBDA): cv.lambda_,
}, cv.has_exactly_one_key(*FILTER_KEYS))])
}, cv.has_exactly_one_key(*FILTER_KEYS))
MULTI_CLICK_TIMING_SCHEMA = vol.Schema({
vol.Optional(CONF_STATE): cv.boolean,
@ -181,6 +182,9 @@ BINARY_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend({
validate_multi_click_timing),
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(
"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]))
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)

View File

@ -13,9 +13,9 @@ PLATFORM_SCHEMA = binary_sensor.PLATFORM_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(CustomBinarySensorConstructor),
vol.Required(CONF_LAMBDA): cv.lambda_,
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),
})]),
})),
})

View File

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

View File

@ -12,9 +12,9 @@ MULTI_CONF = True
CONFIG_SCHEMA = vol.Schema({
cv.GenerateID(): cv.declare_variable_id(CustomComponentConstructor),
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)
}).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,
cv.one_of(*WAKEUP_PIN_MODES), upper=True),
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],
[validate_pin_number]),
vol.Required(CONF_PINS): cv.ensure_list(pins.shorthand_input_pin, validate_pin_number),
vol.Required(CONF_MODE): cv.one_of(*EXT1_WAKEUP_MODES, upper=True),
})),
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({
cv.GenerateID(CONF_EFFECT_ID): cv.declare_variable_id(StrobeLightEffect),
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_BRIGHTNESS, 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.Required(CONF_DURATION): cv.positive_time_period_milliseconds,
}), 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({
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({
cv.GenerateID(CONF_EFFECT_ID): cv.declare_variable_id(FastLEDColorWipeEffect),
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_GREEN, default=1.0): cv.percentage,
vol.Optional(CONF_BLUE, default=1.0): cv.percentage,
vol.Optional(CONF_RANDOM, default=False): cv.boolean,
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_REVERSE): cv.boolean,
}),
@ -178,7 +178,8 @@ EFFECTS_SCHEMA = vol.Schema({
def validate_effects(allowed_effects):
def validator(value):
is_list = isinstance(value, list)
value = cv.ensure_list(value)
if not is_list:
value = [value]
names = set()
ret = []
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,
white_value=True):
ret = mqtt.build_hass_config(data, 'light', config, include_state=True, include_command=True,
platform='mqtt_json')
ret = mqtt.build_hass_config(data, 'light', config, include_state=True, include_command=True)
if ret is None:
return None
ret['schema'] = 'json'
if brightness:
ret['brightness'] = True
if rgb:

View File

@ -108,7 +108,7 @@ def validate_printf(value):
CONF_LOGGER_LOG = 'logger.log'
LOGGER_LOG_ACTION_SCHEMA = vol.All(maybe_simple_message({
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_TAG, default="main"): cv.string,
}), validate_printf)

View File

@ -85,7 +85,7 @@ CONFIG_SCHEMA = vol.All(vol.Schema({
vol.Optional(CONF_LEVEL): logger.is_log_level,
}), validate_message_just_topic),
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_REBOOT_TIMEOUT): cv.positive_time_period_milliseconds,
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)
def build_hass_config(data, component_type, config, include_state=True, include_command=True,
platform='mqtt'):
def build_hass_config(data, component_type, config, include_state=True, include_command=True):
if config.get(CONF_INTERNAL, False):
return None
ret = OrderedDict()
ret['platform'] = platform
ret['platform'] = 'mqtt'
ret['name'] = config[CONF_NAME]
if include_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),
vol.Required(CONF_LAMBDA): cv.lambda_,
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),
})]),
})),
})
FLOAT_SCHEMA = output.PLATFORM_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(CustomFloatOutputConstructor),
vol.Required(CONF_LAMBDA): cv.lambda_,
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),
})]),
})),
})

View File

@ -41,8 +41,7 @@ CONFIG_SCHEMA = vol.Schema({
cv.GenerateID(): cv.declare_variable_id(RemoteReceiverComponent),
vol.Required(CONF_PIN): pins.gpio_input_pin_schema,
vol.Optional(CONF_DUMP, default=[]):
vol.Any(validate_dumpers_all,
vol.All(cv.ensure_list, [cv.one_of(*DUMPERS, lower=True)])),
vol.Any(validate_dumpers_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_BUFFER_SIZE): cv.validate_bytes,
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_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_MULTIPLY): 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_DEBOUNCE): cv.positive_time_period_milliseconds,
vol.Optional(CONF_OR): validate_recursive_filter,
}, cv.has_exactly_one_key(*FILTER_KEYS))])
}, cv.has_exactly_one_key(*FILTER_KEYS))
# Base
sensor_ns = esphomelib_ns.namespace('sensor')

View File

@ -11,9 +11,9 @@ CustomSensorConstructor = sensor.sensor_ns.class_('CustomSensorConstructor')
PLATFORM_SCHEMA = sensor.PLATFORM_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(CustomSensorConstructor),
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),
})]),
})),
})

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),
vol.Required(CONF_LAMBDA): cv.lambda_,
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),
})]),
})),
})

View File

@ -12,9 +12,9 @@ PLATFORM_SCHEMA = text_sensor.PLATFORM_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(CustomTextSensorConstructor),
vol.Required(CONF_LAMBDA): cv.lambda_,
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),
})]),
})),
})

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 esphomeyaml.config_validation as cv
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_helpers import setup_component
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({
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_LAMBDA): cv.lambda_,
vol.Optional(CONF_SERVERS): vol.All(cv.ensure_list(cv.domain), vol.Length(min=1, max=3)),
}).extend(cv.COMPONENT_SCHEMA.schema)

View File

@ -3,12 +3,25 @@ import voluptuous as vol
import esphomeyaml.config_validation as cv
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_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, CONF_SUBNET
from esphomeyaml.core import CORE
from esphomeyaml.cpp_generator import Pvariable, StructInitializer, add
CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, CONF_SUBNET, CONF_NETWORKS, CONF_BSSID
from esphomeyaml.core import CORE, HexInt
from esphomeyaml.cpp_generator import Pvariable, StructInitializer, add, variable, ArrayInitializer
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):
value = cv.string(value)
if not value:
@ -41,10 +54,11 @@ STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend({
})
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_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({
@ -53,35 +67,39 @@ WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend({
WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend({
vol.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA,
vol.Optional(CONF_BSSID): cv.mac_address,
})
def validate(config):
if CONF_PASSWORD in config and CONF_SSID not in config:
raise vol.Invalid("Cannot have WiFi password without SSID!")
if (CONF_SSID 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 "
"to create.")
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({
cv.GenerateID(): cv.declare_variable_id(WiFiComponent),
vol.Optional(CONF_NETWORKS): cv.ensure_list(WIFI_NETWORK_STA),
vol.Optional(CONF_SSID): cv.ssid,
vol.Optional(CONF_PASSWORD): validate_password,
vol.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA,
vol.Optional(CONF_AP): WIFI_NETWORK_AP,
vol.Optional(CONF_HOSTNAME): cv.hostname,
vol.Optional(CONF_DOMAIN, default='.local'): cv.domain_name,
@ -110,21 +128,28 @@ def manual_ip(config):
def wifi_network(config):
return StructInitializer(
WiFiAp,
('ssid', config.get(CONF_SSID, "")),
('password', config.get(CONF_PASSWORD, "")),
('channel', config.get(CONF_CHANNEL, -1)),
('manual_ip', manual_ip(config.get(CONF_MANUAL_IP))),
)
ap = variable(config[CONF_ID], WiFiAP())
if CONF_SSID in config:
add(ap.set_ssid(config[CONF_SSID]))
if CONF_PASSWORD in config:
add(ap.set_password(config[CONF_PASSWORD]))
if CONF_BSSID in config:
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):
rhs = App.init_wifi()
wifi = Pvariable(config[CONF_ID], rhs)
if CONF_SSID in config:
add(wifi.set_sta(wifi_network(config)))
for network in config.get(CONF_NETWORKS, []):
add(wifi.add_sta(wifi_network(network)))
if CONF_AP in config:
add(wifi.set_ap(wifi_network(config[CONF_AP])))

View File

@ -102,13 +102,25 @@ def boolean(value):
return bool(value)
def ensure_list(value):
def ensure_list(*validators):
"""Wrap value in list if it is not one."""
if value is None or (isinstance(value, dict) and not value):
return []
if isinstance(value, list):
return value
return [value]
user = vol.All(*validators)
def validator(value):
if value is None or (isinstance(value, dict) and not 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):
@ -469,16 +481,18 @@ def ssid(value):
raise vol.Invalid("SSID must be a string. Did you wrap it in quotes?")
if not value:
raise vol.Invalid("SSID can't be empty.")
if len(value) > 31:
raise vol.Invalid("SSID can't be longer than 31 characters")
if len(value) > 32:
raise vol.Invalid("SSID can't be longer than 32 characters")
return value
def ipv4(value):
if isinstance(value, list):
parts = value
elif isinstance(value, str):
elif isinstance(value, basestring):
parts = value.split('.')
elif isinstance(value, IPAddress):
return value
else:
raise vol.Invalid("IPv4 address must consist of either string or "
"integer list")
@ -664,6 +678,16 @@ def file_(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):
def __init__(self, key=CONF_ID):
super(GenerateID, self).__init__(key, default=lambda: None)

View File

@ -28,6 +28,7 @@ CONF_BRANCH = 'branch'
CONF_LOGGER = 'logger'
CONF_WIFI = 'wifi'
CONF_SSID = 'ssid'
CONF_BSSID = 'bssid'
CONF_PASSWORD = 'password'
CONF_MANUAL_IP = 'manual_ip'
CONF_STATIC_IP = 'static_ip'
@ -222,6 +223,7 @@ CONF_ACCURACY = 'accuracy'
CONF_BOARD_FLASH_MODE = 'board_flash_mode'
CONF_ON_PRESS = 'on_press'
CONF_ON_RELEASE = 'on_release'
CONF_ON_STATE = 'on_state'
CONF_ON_CLICK = 'on_click'
CONF_ON_DOUBLE_CLICK = 'on_double_click'
CONF_ON_MULTI_CLICK = 'on_multi_click'
@ -386,6 +388,10 @@ CONF_PIN_D = 'pin_d'
CONF_SLEEP_WHEN_DONE = 'sleep_when_done'
CONF_STEP_MODE = 'step_mode'
CONF_COMPONENTS = 'components'
CONF_DATA_TEMPLATE = 'data_template'
CONF_VARIABLES = 'variables'
CONF_SERVICE = 'service'
CONF_ENTITY_ID = 'entity_id'
ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_'
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
class EsphomeyamlCore(object):
def __init__(self):
# True if command is run from dashboard
self.dashboard = False
# The name of the node
self.name = None # type: str
# 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({
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(LoopTrigger),
}),
vol.Optional(CONF_INCLUDES): vol.All(cv.ensure_list, [cv.file_]),
vol.Optional(CONF_LIBRARIES): vol.All(cv.ensure_list, [cv.string_strict]),
vol.Optional(CONF_INCLUDES): cv.ensure_list(cv.file_),
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 "
"was moved into the esphomelib_version option."),

View File

@ -103,49 +103,49 @@ class EsphomeyamlLogsHandler(EsphomeyamlCommandWebSocket):
def build_command(self, message):
js = json.loads(message)
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):
def build_command(self, message):
js = json.loads(message)
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):
def build_command(self, message):
js = json.loads(message)
config_file = os.path.join(CONFIG_DIR, js['configuration'])
return ["esphomeyaml", config_file, "compile"]
return ["esphomeyaml", "--dashboard", config_file, "compile"]
class EsphomeyamlValidateHandler(EsphomeyamlCommandWebSocket):
def build_command(self, message):
js = json.loads(message)
config_file = os.path.join(CONFIG_DIR, js['configuration'])
return ["esphomeyaml", config_file, "config"]
return ["esphomeyaml", "--dashboard", config_file, "config"]
class EsphomeyamlCleanMqttHandler(EsphomeyamlCommandWebSocket):
def build_command(self, message):
js = json.loads(message)
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):
def build_command(self, message):
js = json.loads(message)
config_file = os.path.join(CONFIG_DIR, js['configuration'])
return ["esphomeyaml", config_file, "clean"]
return ["esphomeyaml", "--dashboard", config_file, "clean"]
class EsphomeyamlHassConfigHandler(EsphomeyamlCommandWebSocket):
def build_command(self, message):
js = json.loads(message)
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):
@ -294,10 +294,9 @@ class MainRequestHandler(BaseHandler):
version = const.__version__
docs_link = 'https://beta.esphomelib.com/esphomeyaml/' if 'b' in version else \
'https://esphomelib.com/esphomeyaml/'
mqtt_config = get_mqtt_config_lazy()
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):
@ -497,43 +496,6 @@ def make_app(debug=False):
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):
global CONFIG_DIR
global PASSWORD_DIGEST

View File

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

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<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 rel="stylesheet" href="/static/materialize.min.css?v=1">
<link rel="stylesheet" href="/static/materialize-stepper.min.css?v=1">
@ -22,7 +22,7 @@
<header>
<nav>
<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">
<select></select>
</div>
@ -33,7 +33,7 @@
<div class="tap-target-content">
<h5>Select Upload Port</h5>
<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
for new serial ports to be detected.
</p>
@ -81,7 +81,7 @@
<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-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>
</div>
</div>
@ -164,8 +164,8 @@
<div class="step-content">
<div class="row">
<p>
Hi there! I'm the esphomeyaml setup wizard and will guide you through setting up
your first ESP8266 or ESP32-powered device using esphomeyaml.
Hi there! I'm the ESPHome setup wizard and will guide you through setting up
your first ESP8266 or ESP32-powered device using ESPHome.
</p>
<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>)
@ -174,19 +174,19 @@
such as the <a href="https://esphomelib.com/esphomeyaml/devices/nodemcu_esp8266.html" target="_blank">NodeMCU</a>.
<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
files (similar to the ones you might be used to with Home Assistant).
<p>
</p>
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
<a href="https://github.com/OttoWinter/esphomelib" target="_blank">esphomelib's</a>
Later, you will be able to customize this file and add some of ESPHome's
many integrations.
<p>
<p>
First, I need to know what this node should be called. Choose this name wisely, changing this
later makes Over-The-Air Update attempts difficult.
First, I need to know what this node should be called. Choose this name wisely, it should be unique among
all your ESPs.
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>)
</p>
@ -321,73 +321,15 @@
<label for="wifi_password">WiFi Password</label>
</div>
<p>
Esphomelib automatically sets up an Over-The-Air update server on the node
so that you only need to flash a firmware via USB once.
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. This password
is also used to connect to the ESP from Home Assistant.
Optionally, you can set a password for this upload process here:
</p>
<div class="input-field col s12">
<input id="ota_password" class="validate" name="ota_password" type="password">
<label for="ota_password">OTA 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>
<input id="password" class="validate" name="password" type="password">
<label for="password">Access Password</label>
</div>
</div>
<div class="step-actions">
@ -399,7 +341,7 @@
<div class="step-title waves-effect">Done!</div>
<div class="step-content">
<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
<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
@ -421,7 +363,7 @@
</a>.
</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.
</li>
<li>
@ -429,8 +371,8 @@
have time, I would be happy to help with issues and discuss new features.
</li>
<li>
Star <a href="https://github.com/OttoWinter/esphomelib" target="_blank">esphomelib</a> and
<a href="https://github.com/OttoWinter/esphomeyaml" target="_blank">esphomeyaml</a> on GitHub
Star <a href="https://github.com/OttoWinter/esphomelib" target="_blank">ESPHome Core</a> and
<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.
</li>
</ul>
@ -508,7 +450,7 @@
<div class="tap-target-content">
<h5>Set up your first Node</h5>
<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 🎉
</p>
</div>
@ -522,7 +464,7 @@
<div class="footer-copyright">
<div class="container">
© 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>
</footer>

View File

@ -3,8 +3,10 @@ import logging
import random
import socket
import sys
import time
from esphomeyaml.core import EsphomeyamlError
from esphomeyaml.helpers import resolve_ip_address, is_ip_address
RESPONSE_OK = 0
RESPONSE_REQUEST_AUTH = 1
@ -221,50 +223,26 @@ def perform_ota(sock, password, file_handle, filename):
_LOGGER.info("OTA successful")
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):
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
# Do not connect logs until it is fully on
time.sleep(2)
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.settimeout(10.0)
try:

View File

@ -3,6 +3,7 @@ from __future__ import print_function
import errno
import logging
import os
import socket
import subprocess
_LOGGER = logging.getLogger(__name__)
@ -75,3 +76,26 @@ def mkdir_p(path):
pass
else:
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=""):
from esphomeyaml.core import CORE
if CORE.dashboard:
try:
message = message.replace('\033', '\\033')
except UnicodeEncodeError:
pass
try:
print(message)
return
@ -48,6 +56,29 @@ def shlex_quote(s):
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 mock_exit(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)
_LOGGER.info(u"Running: %s", full_cmd)
sys.stdout = RedirectText(sys.stdout)
sys.stderr = RedirectText(sys.stderr)
capture_stdout = kwargs.get('capture_stdout', False)
if capture_stdout:
sys.stdout = io.BytesIO()
@ -76,6 +110,11 @@ def run_external_command(func, *cmd, **kwargs):
sys.argv = orig_argv
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:
# pylint: disable=lost-exception
stdout = sys.stdout.getvalue()

View File

@ -35,13 +35,6 @@ WIFI_BIG = """ __ ___ ______ _
\ /\ / | | | | |
\/ \/ |_|_| |_|
"""
MQTT_BIG = """ __ __ ____ _______ _______
| \/ |/ __ \__ __|__ __|
| \ / | | | | | | | |
| |\/| | | | | | | | |
| | | | |__| | | | | |
|_| |_|\___\_\ |_| |_|
"""
OTA_BIG = """ ____ _______
/ __ \__ __|/\\
| | | | | | / \\
@ -50,7 +43,6 @@ OTA_BIG = """ ____ _______
\____/ |_/_/ \_\\
"""
# TODO handle escaping
BASE_CONFIG = u"""esphomeyaml:
name: {name}
platform: {platform}
@ -60,24 +52,21 @@ wifi:
ssid: '{ssid}'
password: '{psk}'
mqtt:
broker: '{broker}'
username: '{mqtt_username}'
password: '{mqtt_password}'
# Enable logging
logger:
# Enable Home Assistant API
api:
"""
def wizard_file(**kwargs):
config = BASE_CONFIG.format(**kwargs)
if kwargs['ota_password']:
config += u"ota:\n password: '{}'\n".format(kwargs['ota_password'])
if kwargs['password']:
config += u" password: '{0}'\n\nota:\n password: '{0}'\n".format(kwargs['password'])
else:
config += u"ota:\n"
config += u"\nota:\n"
return config
@ -135,11 +124,11 @@ def wizard(path):
return 1
safe_print("Hi there!")
sleep(1.5)
safe_print("I'm the wizard of esphomeyaml :)")
safe_print("I'm the wizard of ESPHome :)")
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)
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!")
sleep(3.0)
safe_print()
@ -205,6 +194,8 @@ def wizard(path):
else:
safe_print("For example \"{}\".".format(color("bold_white", 'nodemcuv2')))
boards = list(ESP8266_BOARD_PINS.keys())
safe_print("Options: {}".format(', '.join(boards)))
while True:
board = raw_input(color("bold_white", "(board): "))
try:
@ -214,7 +205,6 @@ def wizard(path):
safe_print(color('red', "Sorry, I don't think the board \"{}\" exists."))
safe_print()
sleep(0.25)
safe_print("Possible options are {}".format(', '.join(boards)))
safe_print()
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).")
sleep(1.5)
safe_print_step(4, MQTT_BIG)
safe_print("Almost there! Now let's setup MQTT so that your node can connect to the "
"outside world.")
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_step(4, OTA_BIG)
safe_print("Almost there! ESPHome can automatically upload custom firmwares over WiFi "
"(over the air) and integrates into Home Assistant with a native API.")
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()
sleep(0.25)
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,
ssid=ssid, psk=psk, broker=broker,
mqtt_username=mqtt_username, mqtt_password=mqtt_password,
ota_password=ota_password)
ssid=ssid, psk=psk, password=password)
safe_print()
safe_print(color('cyan', "DONE! I've now written a new configuration file to ") +
color('bold_cyan', path))
safe_print()
safe_print("Next steps:")
safe_print(" > If you haven't already, enable MQTT discovery in Home Assistant:")
safe_print()
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(" > Check your Home Assistant \"integrations\" screen. If all goes well, you "
"should see your ESP being discovered automatically.")
safe_print(" > Then follow the rest of the getting started guide:")
safe_print(" > https://esphomelib.com/esphomeyaml/guides/getting_started_command_line.html")
return 0

View File

@ -279,6 +279,7 @@ def gather_lib_deps():
# Manual fix for AsyncTCP
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.remove('AsyncTCP@1.0.1')
# avoid changing build flags order
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("eeprom, data, 0x99, 0x390000, 0x001000,\n")
f.write("spiffs, data, spiffs, 0x391000, 0x00F000\n")
write_gitignore()
write_platformio_ini(content, platformio_ini)
@ -427,3 +429,23 @@ def clean_build():
continue
_LOGGER.info("Deleting %s", 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]
reports=no
ignore=api_pb2.py
disable=
missing-docstring,

View File

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

View File

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

View File

@ -30,6 +30,7 @@ REQUIRES = [
'tornado>=5.0.0',
'esptool>=2.3.1',
'typing>=3.0.0',
'protobuf>=3.4',
]
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