mirror of
https://github.com/esphome/esphome.git
synced 2024-12-22 05:24:53 +01:00
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:
parent
7556845079
commit
da2821ab36
49 changed files with 3783 additions and 346 deletions
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -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
vendored
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -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
vendored
15
.github/ISSUE_TEMPLATE/new-integration.md
vendored
|
@ -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
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Normal file
0
esphomeyaml/api/__init__.py
Normal file
329
esphomeyaml/api/api.proto
Normal file
329
esphomeyaml/api/api.proto
Normal 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
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
474
esphomeyaml/api/client.py
Normal 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
|
|
@ -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
Normal file
85
esphomeyaml/components/api.py
Normal 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
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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),
|
||||
})]),
|
||||
})),
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)),
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -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
Normal file
23
esphomeyaml/components/interval.py
Normal 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)
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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),
|
||||
})]),
|
||||
})),
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
Normal file
32
esphomeyaml/components/sensor/homeassistant.py
Normal 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)
|
|
@ -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),
|
||||
})]),
|
||||
})),
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -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),
|
||||
})]),
|
||||
})),
|
||||
})
|
||||
|
||||
|
||||
|
|
33
esphomeyaml/components/text_sensor/homeassistant.py
Normal file
33
esphomeyaml/components/text_sensor/homeassistant.py
Normal 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)
|
25
esphomeyaml/components/time/homeassistant.py
Normal file
25
esphomeyaml/components/time/homeassistant.py
Normal 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'
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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])))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"><HASS_CONFIG_FOLDER>/esphomeyaml/<NAME_OF_NODE>.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>
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
1
pylintrc
1
pylintrc
|
@ -1,5 +1,6 @@
|
|||
[MASTER]
|
||||
reports=no
|
||||
ignore=api_pb2.py
|
||||
|
||||
disable=
|
||||
missing-docstring,
|
||||
|
|
|
@ -6,3 +6,4 @@ colorlog>=3.1.2
|
|||
tornado>=5.0.0
|
||||
esptool>=2.3.1
|
||||
typing>=3.0.0
|
||||
protobuf>=3.4
|
||||
|
|
|
@ -4,3 +4,4 @@ description-file = README.md
|
|||
[flake8]
|
||||
max-line-length = 120
|
||||
builtins = unicode, long, raw_input
|
||||
exclude = api_pb2.py
|
||||
|
|
1
setup.py
1
setup.py
|
@ -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
10
tests/.gitignore
vendored
Normal 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
|
Loading…
Reference in a new issue