Browse Source

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
pull/273/head
Otto Winter 3 years ago
committed by GitHub
parent
commit
da2821ab36
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 17
      .github/ISSUE_TEMPLATE/feature_request.md
  3. 15
      .github/ISSUE_TEMPLATE/new-integration.md
  4. 6
      .github/PULL_REQUEST_TEMPLATE.md
  5. 5
      esphomeyaml-edge/config.json
  6. 8
      esphomeyaml/__main__.py
  7. 0
      esphomeyaml/api/__init__.py
  8. 329
      esphomeyaml/api/api.proto
  9. 2445
      esphomeyaml/api/api_pb2.py
  10. 474
      esphomeyaml/api/client.py
  11. 5
      esphomeyaml/automation.py
  12. 85
      esphomeyaml/components/api.py
  13. 15
      esphomeyaml/components/binary_sensor/__init__.py
  14. 4
      esphomeyaml/components/binary_sensor/custom.py
  15. 1
      esphomeyaml/components/binary_sensor/status.py
  16. 4
      esphomeyaml/components/custom_component.py
  17. 3
      esphomeyaml/components/deep_sleep.py
  18. 23
      esphomeyaml/components/interval.py
  19. 15
      esphomeyaml/components/light/__init__.py
  20. 2
      esphomeyaml/components/logger.py
  21. 7
      esphomeyaml/components/mqtt.py
  22. 8
      esphomeyaml/components/output/custom.py
  23. 3
      esphomeyaml/components/remote_receiver.py
  24. 4
      esphomeyaml/components/sensor/__init__.py
  25. 4
      esphomeyaml/components/sensor/custom.py
  26. 32
      esphomeyaml/components/sensor/homeassistant.py
  27. 4
      esphomeyaml/components/switch/custom.py
  28. 4
      esphomeyaml/components/text_sensor/custom.py
  29. 33
      esphomeyaml/components/text_sensor/homeassistant.py
  30. 25
      esphomeyaml/components/time/homeassistant.py
  31. 7
      esphomeyaml/components/time/sntp.py
  32. 79
      esphomeyaml/components/wifi.py
  33. 42
      esphomeyaml/config_validation.py
  34. 6
      esphomeyaml/const.py
  35. 2
      esphomeyaml/core.py
  36. 4
      esphomeyaml/core_config.py
  37. 54
      esphomeyaml/dashboard/dashboard.py
  38. 4
      esphomeyaml/dashboard/static/esphomeyaml.js
  39. 104
      esphomeyaml/dashboard/templates/index.html
  40. 58
      esphomeyaml/espota2.py
  41. 24
      esphomeyaml/helpers.py
  42. 39
      esphomeyaml/util.py
  43. 80
      esphomeyaml/wizard.py
  44. 22
      esphomeyaml/writer.py
  45. 1
      pylintrc
  46. 1
      requirements.txt
  47. 1
      setup.cfg
  48. 1
      setup.py
  49. 10
      tests/.gitignore

8
.github/ISSUE_TEMPLATE/bug_report.md

@ -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):**

17
.github/ISSUE_TEMPLATE/feature_request.md

@ -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. -->

15
.github/ISSUE_TEMPLATE/new-integration.md

@ -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!

6
.github/PULL_REQUEST_TEMPLATE.md

@ -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).

5
esphomeyaml-edge/config.json

@ -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

8
esphomeyaml/__main__.py

@ -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:

0
esphomeyaml/api/__init__.py

329
esphomeyaml/api/api.proto

@ -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

File diff suppressed because one or more lines are too long

474
esphomeyaml/api/client.py

@ -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

5
esphomeyaml/automation.py

@ -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)

85
esphomeyaml/components/api.py

@ -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

15
esphomeyaml/components/binary_sensor/__init__.py

@ -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)

4
esphomeyaml/components/binary_sensor/custom.py

@ -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),
})]),
})),
})

1
esphomeyaml/components/binary_sensor/status.py

@ -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',

4
esphomeyaml/components/custom_component.py

@ -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)),
})

3
esphomeyaml/components/deep_sleep.py

@ -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,

23
esphomeyaml/components/interval.py

@ -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)

15
esphomeyaml/components/light/__init__.py

@ -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:

2
esphomeyaml/components/logger.py

@ -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)

7
esphomeyaml/components/mqtt.py

@ -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')

8
esphomeyaml/components/output/custom.py

@ -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),
})]),
})),
})

3
esphomeyaml/components/remote_receiver.py

@ -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,

4
esphomeyaml/components/sensor/__init__.py

@ -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')

4
esphomeyaml/components/sensor/custom.py

@ -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),
})]),
})),
})

32
esphomeyaml/components/sensor/homeassistant.py

@ -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)

4
esphomeyaml/components/switch/custom.py