mirror of
https://github.com/esphome/esphome.git
synced 2024-11-25 16:38:16 +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
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help esphomelib improve
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -9,7 +9,9 @@ about: Create a report to help us improve
|
||||||
- esphomeyaml [here] - This is mostly for reporting bugs when compiling and when you get a long stack trace while compiling or if a configuration fails to validate.
|
- esphomeyaml [here] - This is mostly for reporting bugs when compiling and when you get a long stack trace while compiling or if a configuration fails to validate.
|
||||||
- esphomelib [https://github.com/OttoWinter/esphomelib] - Report bugs there if the ESP is crashing or a feature is not working as expected.
|
- esphomelib [https://github.com/OttoWinter/esphomelib] - Report bugs there if the ESP is crashing or a feature is not working as expected.
|
||||||
- esphomedocs [https://github.com/OttoWinter/esphomedocs] - Report bugs there if the documentation is wrong/outdated.
|
- esphomedocs [https://github.com/OttoWinter/esphomedocs] - Report bugs there if the documentation is wrong/outdated.
|
||||||
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks (```). Do not delete any text from this template!
|
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks (```).
|
||||||
|
|
||||||
|
DO NOT DELETE ANY TEXT from this template! Otherwise the issue may be closed without a comment.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
**Operating environment (Hass.io/Docker/pip/etc.):**
|
**Operating environment (Hass.io/Docker/pip/etc.):**
|
||||||
|
@ -33,7 +35,7 @@ Please add the link to the documentation at https://esphomelib.com/esphomeyaml/i
|
||||||
|
|
||||||
**Problem-relevant YAML-configuration entries:**
|
**Problem-relevant YAML-configuration entries:**
|
||||||
```yaml
|
```yaml
|
||||||
|
PASTE YAML FILE HERE
|
||||||
```
|
```
|
||||||
|
|
||||||
**Traceback (if applicable):**
|
**Traceback (if applicable):**
|
||||||
|
|
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:
|
<!-- READ THIS FIRST:
|
||||||
- This is for feature requests only, if you want to have a certain new sensor/module supported, please use the "new integration" template.
|
- This is for feature requests only, if you want to have a certain new sensor/module supported, please use the "new integration" template.
|
||||||
- Please be as descriptive as possible, especially use-cases that can otherwise not be solved boost the problem's priority.
|
- Please be as descriptive as possible, especially use-cases that can otherwise not be solved boost the problem's priority.
|
||||||
|
|
||||||
|
DO NOT DELETE ANY TEXT from this template! Otherwise the issue may be closed without a comment.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
**Is your feature request related to a problem/use-case? Please describe.**
|
||||||
<!--
|
<!-- A clear and concise description of what the problem is. -->
|
||||||
A clear and concise description of what the problem is.
|
|
||||||
-->
|
|
||||||
Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
**Describe the solution you'd like:**
|
||||||
A description of what you want to happen.
|
<!-- A description of what you want to happen. -->
|
||||||
|
|
||||||
**Additional context**
|
**Additional context:**
|
||||||
Add any other context about the feature request here.
|
<!-- Add any other context about the feature request here. -->
|
||||||
|
|
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:
|
DO NOT POST NEW INTEGRATION REQUESTS HERE!
|
||||||
- This is for new integrations (such as new sensors/modules) only, for new features within the environment please use the "feature request" template.
|
|
||||||
- Do not delete anything from this template and fill out the form as precisely as possible.
|
|
||||||
-->
|
|
||||||
|
|
||||||
**What new integration would you wish to have?**
|
Please post all new integration requests in the esphomelib repository:
|
||||||
<!-- A name/description of the new integration/board. -->
|
|
||||||
|
|
||||||
**If possible, provide a link to an existing library for the integration:**
|
https://github.com/OttoWinter/esphomelib/issues
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
Thank you!
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
|
|
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 [esphomedocs](https://github.com/OttoWinter/esphomedocs) with documentation (if applicable):** OttoWinter/esphomedocs#<esphomedocs PR number goes here>
|
||||||
**Pull request in [esphomelib](https://github.com/OttoWinter/esphomelib) with C++ framework changes (if applicable):** OttoWinter/esphomelib#<esphomelib PR number goes here>
|
**Pull request in [esphomelib](https://github.com/OttoWinter/esphomelib) with C++ framework changes (if applicable):** OttoWinter/esphomelib#<esphomelib PR number goes here>
|
||||||
|
|
||||||
## Example entry for YAML configuration (if applicable):
|
|
||||||
```yaml
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Checklist:
|
## Checklist:
|
||||||
- [ ] The code change is tested and works locally.
|
- [ ] The code change is tested and works locally.
|
||||||
- [ ] Tests have been added to verify that the new code works (under `tests/` folder).
|
- [ ] Tests have been added to verify that the new code works (under `tests/` folder).
|
||||||
- [ ] Check this box if you have read, understand, comply, and agree with the [Code of Conduct](https://github.com/OttoWinter/esphomeyaml/blob/master/CODE_OF_CONDUCT.md).
|
|
||||||
|
|
||||||
If user exposed functionality or configuration variables are added/changed:
|
If user exposed functionality or configuration variables are added/changed:
|
||||||
- [ ] Documentation added/updated in [esphomedocs](https://github.com/OttoWinter/esphomedocs).
|
- [ ] Documentation added/updated in [esphomedocs](https://github.com/OttoWinter/esphomedocs).
|
||||||
|
|
|
@ -14,12 +14,9 @@
|
||||||
],
|
],
|
||||||
"hassio_api": true,
|
"hassio_api": true,
|
||||||
"auth_api": true,
|
"auth_api": true,
|
||||||
"services": [
|
|
||||||
"mqtt:want"
|
|
||||||
],
|
|
||||||
"hassio_role": "default",
|
"hassio_role": "default",
|
||||||
"homeassistant_api": false,
|
"homeassistant_api": false,
|
||||||
"host_network": false,
|
"host_network": true,
|
||||||
"boot": "auto",
|
"boot": "auto",
|
||||||
"ports": {
|
"ports": {
|
||||||
"6052/tcp": 6052
|
"6052/tcp": 6052
|
||||||
|
|
|
@ -9,6 +9,7 @@ import random
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from esphomeyaml import const, core_config, mqtt, platformio_api, wizard, writer, yaml_util
|
from esphomeyaml import const, core_config, mqtt, platformio_api, wizard, writer, yaml_util
|
||||||
|
from esphomeyaml.api.client import run_logs
|
||||||
from esphomeyaml.config import get_component, iter_components, read_config, strip_default_ids
|
from esphomeyaml.config import get_component, iter_components, read_config, strip_default_ids
|
||||||
from esphomeyaml.const import CONF_BAUD_RATE, CONF_DOMAIN, CONF_ESPHOMEYAML, \
|
from esphomeyaml.const import CONF_BAUD_RATE, CONF_DOMAIN, CONF_ESPHOMEYAML, \
|
||||||
CONF_HOSTNAME, CONF_LOGGER, CONF_MANUAL_IP, CONF_NAME, CONF_STATIC_IP, CONF_USE_CUSTOM_CODE, \
|
CONF_HOSTNAME, CONF_LOGGER, CONF_MANUAL_IP, CONF_NAME, CONF_STATIC_IP, CONF_USE_CUSTOM_CODE, \
|
||||||
|
@ -22,7 +23,7 @@ from esphomeyaml.util import run_external_command, safe_print
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
PRE_INITIALIZE = ['esphomeyaml', 'logger', 'wifi', 'ota', 'mqtt', 'web_server', 'i2c']
|
PRE_INITIALIZE = ['esphomeyaml', 'logger', 'wifi', 'ota', 'mqtt', 'web_server', 'api', 'i2c']
|
||||||
|
|
||||||
|
|
||||||
def get_serial_ports():
|
def get_serial_ports():
|
||||||
|
@ -202,6 +203,8 @@ def show_logs(config, args, port):
|
||||||
if port != 'OTA' and serial_port:
|
if port != 'OTA' and serial_port:
|
||||||
run_miniterm(config, port)
|
run_miniterm(config, port)
|
||||||
return 0
|
return 0
|
||||||
|
if 'api' in config:
|
||||||
|
return run_logs(config, get_upload_host(config))
|
||||||
return mqtt.show_logs(config, args.topic, args.username, args.password, args.client_id)
|
return mqtt.show_logs(config, args.topic, args.username, args.password, args.client_id)
|
||||||
|
|
||||||
|
|
||||||
|
@ -368,6 +371,8 @@ def parse_args(argv):
|
||||||
parser = argparse.ArgumentParser(prog='esphomeyaml')
|
parser = argparse.ArgumentParser(prog='esphomeyaml')
|
||||||
parser.add_argument('-v', '--verbose', help="Enable verbose esphomeyaml logs.",
|
parser.add_argument('-v', '--verbose', help="Enable verbose esphomeyaml logs.",
|
||||||
action='store_true')
|
action='store_true')
|
||||||
|
parser.add_argument('--dashboard', help="Internal flag to set if the command is run from the "
|
||||||
|
"dashboard.", action='store_true')
|
||||||
parser.add_argument('configuration', help='Your YAML configuration file.')
|
parser.add_argument('configuration', help='Your YAML configuration file.')
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(help='Commands', dest='command')
|
subparsers = parser.add_subparsers(help='Commands', dest='command')
|
||||||
|
@ -445,6 +450,7 @@ def parse_args(argv):
|
||||||
|
|
||||||
def run_esphomeyaml(argv):
|
def run_esphomeyaml(argv):
|
||||||
args = parse_args(argv)
|
args = parse_args(argv)
|
||||||
|
CORE.dashboard = args.dashboard
|
||||||
|
|
||||||
setup_log(args.verbose)
|
setup_log(args.verbose)
|
||||||
if args.command in PRE_CONFIG_ACTIONS:
|
if args.command in PRE_CONFIG_ACTIONS:
|
||||||
|
|
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):
|
def validate_recursive_condition(value):
|
||||||
is_list = isinstance(value, list)
|
is_list = isinstance(value, list)
|
||||||
value = cv.ensure_list(value)[:]
|
value = cv.ensure_list()(value)[:]
|
||||||
for i, item in enumerate(value):
|
for i, item in enumerate(value):
|
||||||
path = [i] if is_list else []
|
path = [i] if is_list else []
|
||||||
item = copy.deepcopy(item)
|
item = copy.deepcopy(item)
|
||||||
|
@ -61,7 +61,8 @@ def validate_recursive_condition(value):
|
||||||
|
|
||||||
def validate_recursive_action(value):
|
def validate_recursive_action(value):
|
||||||
is_list = isinstance(value, list)
|
is_list = isinstance(value, list)
|
||||||
value = cv.ensure_list(value)[:]
|
if not is_list:
|
||||||
|
value = [value]
|
||||||
for i, item in enumerate(value):
|
for i, item in enumerate(value):
|
||||||
path = [i] if is_list else []
|
path = [i] if is_list else []
|
||||||
item = copy.deepcopy(item)
|
item = copy.deepcopy(item)
|
||||||
|
|
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_HEARTBEAT, CONF_ID, CONF_INTERNAL, CONF_INVALID_COOLDOWN, CONF_INVERT, CONF_INVERTED, \
|
||||||
CONF_LAMBDA, CONF_MAX_LENGTH, CONF_MIN_LENGTH, CONF_MQTT_ID, CONF_ON_CLICK, \
|
CONF_LAMBDA, CONF_MAX_LENGTH, CONF_MIN_LENGTH, CONF_MQTT_ID, CONF_ON_CLICK, \
|
||||||
CONF_ON_DOUBLE_CLICK, CONF_ON_MULTI_CLICK, CONF_ON_PRESS, CONF_ON_RELEASE, CONF_STATE, \
|
CONF_ON_DOUBLE_CLICK, CONF_ON_MULTI_CLICK, CONF_ON_PRESS, CONF_ON_RELEASE, CONF_STATE, \
|
||||||
CONF_TIMING, CONF_TRIGGER_ID
|
CONF_TIMING, CONF_TRIGGER_ID, CONF_ON_STATE
|
||||||
from esphomeyaml.core import CORE
|
from esphomeyaml.core import CORE
|
||||||
from esphomeyaml.cpp_generator import process_lambda, ArrayInitializer, add, Pvariable, \
|
from esphomeyaml.cpp_generator import process_lambda, ArrayInitializer, add, Pvariable, \
|
||||||
StructInitializer, get_variable
|
StructInitializer, get_variable
|
||||||
|
@ -38,6 +38,7 @@ ClickTrigger = binary_sensor_ns.class_('ClickTrigger', Trigger.template(NoArg))
|
||||||
DoubleClickTrigger = binary_sensor_ns.class_('DoubleClickTrigger', Trigger.template(NoArg))
|
DoubleClickTrigger = binary_sensor_ns.class_('DoubleClickTrigger', Trigger.template(NoArg))
|
||||||
MultiClickTrigger = binary_sensor_ns.class_('MultiClickTrigger', Trigger.template(NoArg), Component)
|
MultiClickTrigger = binary_sensor_ns.class_('MultiClickTrigger', Trigger.template(NoArg), Component)
|
||||||
MultiClickTriggerEvent = binary_sensor_ns.struct('MultiClickTriggerEvent')
|
MultiClickTriggerEvent = binary_sensor_ns.struct('MultiClickTriggerEvent')
|
||||||
|
StateTrigger = binary_sensor_ns.class_('StateTrigger', Trigger.template(bool_))
|
||||||
|
|
||||||
# Condition
|
# Condition
|
||||||
BinarySensorCondition = binary_sensor_ns.class_('BinarySensorCondition', Condition)
|
BinarySensorCondition = binary_sensor_ns.class_('BinarySensorCondition', Condition)
|
||||||
|
@ -53,13 +54,13 @@ LambdaFilter = binary_sensor_ns.class_('LambdaFilter', Filter)
|
||||||
|
|
||||||
FILTER_KEYS = [CONF_INVERT, CONF_DELAYED_ON, CONF_DELAYED_OFF, CONF_LAMBDA, CONF_HEARTBEAT]
|
FILTER_KEYS = [CONF_INVERT, CONF_DELAYED_ON, CONF_DELAYED_OFF, CONF_LAMBDA, CONF_HEARTBEAT]
|
||||||
|
|
||||||
FILTERS_SCHEMA = vol.All(cv.ensure_list, [vol.All({
|
FILTERS_SCHEMA = cv.ensure_list({
|
||||||
vol.Optional(CONF_INVERT): None,
|
vol.Optional(CONF_INVERT): None,
|
||||||
vol.Optional(CONF_DELAYED_ON): cv.positive_time_period_milliseconds,
|
vol.Optional(CONF_DELAYED_ON): cv.positive_time_period_milliseconds,
|
||||||
vol.Optional(CONF_DELAYED_OFF): cv.positive_time_period_milliseconds,
|
vol.Optional(CONF_DELAYED_OFF): cv.positive_time_period_milliseconds,
|
||||||
vol.Optional(CONF_HEARTBEAT): cv.positive_time_period_milliseconds,
|
vol.Optional(CONF_HEARTBEAT): cv.positive_time_period_milliseconds,
|
||||||
vol.Optional(CONF_LAMBDA): cv.lambda_,
|
vol.Optional(CONF_LAMBDA): cv.lambda_,
|
||||||
}, cv.has_exactly_one_key(*FILTER_KEYS))])
|
}, cv.has_exactly_one_key(*FILTER_KEYS))
|
||||||
|
|
||||||
MULTI_CLICK_TIMING_SCHEMA = vol.Schema({
|
MULTI_CLICK_TIMING_SCHEMA = vol.Schema({
|
||||||
vol.Optional(CONF_STATE): cv.boolean,
|
vol.Optional(CONF_STATE): cv.boolean,
|
||||||
|
@ -181,6 +182,9 @@ BINARY_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend({
|
||||||
validate_multi_click_timing),
|
validate_multi_click_timing),
|
||||||
vol.Optional(CONF_INVALID_COOLDOWN): cv.positive_time_period_milliseconds,
|
vol.Optional(CONF_INVALID_COOLDOWN): cv.positive_time_period_milliseconds,
|
||||||
}),
|
}),
|
||||||
|
vol.Optional(CONF_ON_STATE): automation.validate_automation({
|
||||||
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(StateTrigger),
|
||||||
|
}),
|
||||||
|
|
||||||
vol.Optional(CONF_INVERTED): cv.invalid(
|
vol.Optional(CONF_INVERTED): cv.invalid(
|
||||||
"The inverted binary_sensor property has been replaced by the "
|
"The inverted binary_sensor property has been replaced by the "
|
||||||
|
@ -268,6 +272,11 @@ def setup_binary_sensor_core_(binary_sensor_var, mqtt_var, config):
|
||||||
add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN]))
|
add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN]))
|
||||||
automation.build_automation(trigger, NoArg, conf)
|
automation.build_automation(trigger, NoArg, conf)
|
||||||
|
|
||||||
|
for conf in config.get(CONF_ON_STATE, []):
|
||||||
|
rhs = binary_sensor_var.make_state_trigger()
|
||||||
|
trigger = Pvariable(conf[CONF_TRIGGER_ID], rhs)
|
||||||
|
automation.build_automation(trigger, bool_, conf)
|
||||||
|
|
||||||
setup_mqtt_component(mqtt_var, config)
|
setup_mqtt_component(mqtt_var, config)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,9 +13,9 @@ PLATFORM_SCHEMA = binary_sensor.PLATFORM_SCHEMA.extend({
|
||||||
cv.GenerateID(): cv.declare_variable_id(CustomBinarySensorConstructor),
|
cv.GenerateID(): cv.declare_variable_id(CustomBinarySensorConstructor),
|
||||||
vol.Required(CONF_LAMBDA): cv.lambda_,
|
vol.Required(CONF_LAMBDA): cv.lambda_,
|
||||||
vol.Required(CONF_BINARY_SENSORS):
|
vol.Required(CONF_BINARY_SENSORS):
|
||||||
vol.All(cv.ensure_list, [binary_sensor.BINARY_SENSOR_SCHEMA.extend({
|
cv.ensure_list(binary_sensor.BINARY_SENSOR_SCHEMA.extend({
|
||||||
cv.GenerateID(): cv.declare_variable_id(binary_sensor.BinarySensor),
|
cv.GenerateID(): cv.declare_variable_id(binary_sensor.BinarySensor),
|
||||||
})]),
|
})),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ from esphomeyaml.cpp_generator import variable
|
||||||
from esphomeyaml.cpp_helpers import setup_component
|
from esphomeyaml.cpp_helpers import setup_component
|
||||||
from esphomeyaml.cpp_types import Application, Component, App
|
from esphomeyaml.cpp_types import Application, Component, App
|
||||||
|
|
||||||
DEPENDENCIES = ['mqtt']
|
|
||||||
|
|
||||||
MakeStatusBinarySensor = Application.struct('MakeStatusBinarySensor')
|
MakeStatusBinarySensor = Application.struct('MakeStatusBinarySensor')
|
||||||
StatusBinarySensor = binary_sensor.binary_sensor_ns.class_('StatusBinarySensor',
|
StatusBinarySensor = binary_sensor.binary_sensor_ns.class_('StatusBinarySensor',
|
||||||
|
|
|
@ -12,9 +12,9 @@ MULTI_CONF = True
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
cv.GenerateID(): cv.declare_variable_id(CustomComponentConstructor),
|
cv.GenerateID(): cv.declare_variable_id(CustomComponentConstructor),
|
||||||
vol.Required(CONF_LAMBDA): cv.lambda_,
|
vol.Required(CONF_LAMBDA): cv.lambda_,
|
||||||
vol.Optional(CONF_COMPONENTS): vol.All(cv.ensure_list, [vol.Schema({
|
vol.Optional(CONF_COMPONENTS): cv.ensure_list(vol.Schema({
|
||||||
cv.GenerateID(): cv.declare_variable_id(Component)
|
cv.GenerateID(): cv.declare_variable_id(Component)
|
||||||
}).extend(cv.COMPONENT_SCHEMA.schema)]),
|
}).extend(cv.COMPONENT_SCHEMA.schema)),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -46,8 +46,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||||
vol.Optional(CONF_WAKEUP_PIN_MODE): vol.All(cv.only_on_esp32,
|
vol.Optional(CONF_WAKEUP_PIN_MODE): vol.All(cv.only_on_esp32,
|
||||||
cv.one_of(*WAKEUP_PIN_MODES), upper=True),
|
cv.one_of(*WAKEUP_PIN_MODES), upper=True),
|
||||||
vol.Optional(CONF_ESP32_EXT1_WAKEUP): vol.All(cv.only_on_esp32, vol.Schema({
|
vol.Optional(CONF_ESP32_EXT1_WAKEUP): vol.All(cv.only_on_esp32, vol.Schema({
|
||||||
vol.Required(CONF_PINS): vol.All(cv.ensure_list, [pins.shorthand_input_pin],
|
vol.Required(CONF_PINS): cv.ensure_list(pins.shorthand_input_pin, validate_pin_number),
|
||||||
[validate_pin_number]),
|
|
||||||
vol.Required(CONF_MODE): cv.one_of(*EXT1_WAKEUP_MODES, upper=True),
|
vol.Required(CONF_MODE): cv.one_of(*EXT1_WAKEUP_MODES, upper=True),
|
||||||
})),
|
})),
|
||||||
vol.Optional(CONF_RUN_CYCLES): cv.positive_int,
|
vol.Optional(CONF_RUN_CYCLES): cv.positive_int,
|
||||||
|
|
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({
|
vol.Optional(CONF_STROBE): vol.Schema({
|
||||||
cv.GenerateID(CONF_EFFECT_ID): cv.declare_variable_id(StrobeLightEffect),
|
cv.GenerateID(CONF_EFFECT_ID): cv.declare_variable_id(StrobeLightEffect),
|
||||||
vol.Optional(CONF_NAME, default="Strobe"): cv.string,
|
vol.Optional(CONF_NAME, default="Strobe"): cv.string,
|
||||||
vol.Optional(CONF_COLORS): vol.All(cv.ensure_list, [vol.All(vol.Schema({
|
vol.Optional(CONF_COLORS): vol.All(cv.ensure_list(vol.Schema({
|
||||||
vol.Optional(CONF_STATE, default=True): cv.boolean,
|
vol.Optional(CONF_STATE, default=True): cv.boolean,
|
||||||
vol.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage,
|
vol.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage,
|
||||||
vol.Optional(CONF_RED, default=1.0): cv.percentage,
|
vol.Optional(CONF_RED, default=1.0): cv.percentage,
|
||||||
|
@ -109,7 +109,7 @@ EFFECTS_SCHEMA = vol.Schema({
|
||||||
vol.Optional(CONF_WHITE, default=1.0): cv.percentage,
|
vol.Optional(CONF_WHITE, default=1.0): cv.percentage,
|
||||||
vol.Required(CONF_DURATION): cv.positive_time_period_milliseconds,
|
vol.Required(CONF_DURATION): cv.positive_time_period_milliseconds,
|
||||||
}), cv.has_at_least_one_key(CONF_STATE, CONF_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE,
|
}), cv.has_at_least_one_key(CONF_STATE, CONF_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE,
|
||||||
CONF_WHITE))], vol.Length(min=2)),
|
CONF_WHITE)), vol.Length(min=2)),
|
||||||
}),
|
}),
|
||||||
vol.Optional(CONF_FLICKER): vol.Schema({
|
vol.Optional(CONF_FLICKER): vol.Schema({
|
||||||
cv.GenerateID(CONF_EFFECT_ID): cv.declare_variable_id(FlickerLightEffect),
|
cv.GenerateID(CONF_EFFECT_ID): cv.declare_variable_id(FlickerLightEffect),
|
||||||
|
@ -131,13 +131,13 @@ EFFECTS_SCHEMA = vol.Schema({
|
||||||
vol.Optional(CONF_FASTLED_COLOR_WIPE): vol.Schema({
|
vol.Optional(CONF_FASTLED_COLOR_WIPE): vol.Schema({
|
||||||
cv.GenerateID(CONF_EFFECT_ID): cv.declare_variable_id(FastLEDColorWipeEffect),
|
cv.GenerateID(CONF_EFFECT_ID): cv.declare_variable_id(FastLEDColorWipeEffect),
|
||||||
vol.Optional(CONF_NAME, default="Color Wipe"): cv.string,
|
vol.Optional(CONF_NAME, default="Color Wipe"): cv.string,
|
||||||
vol.Optional(CONF_COLORS): vol.All(cv.ensure_list, [vol.Schema({
|
vol.Optional(CONF_COLORS): cv.ensure_list({
|
||||||
vol.Optional(CONF_RED, default=1.0): cv.percentage,
|
vol.Optional(CONF_RED, default=1.0): cv.percentage,
|
||||||
vol.Optional(CONF_GREEN, default=1.0): cv.percentage,
|
vol.Optional(CONF_GREEN, default=1.0): cv.percentage,
|
||||||
vol.Optional(CONF_BLUE, default=1.0): cv.percentage,
|
vol.Optional(CONF_BLUE, default=1.0): cv.percentage,
|
||||||
vol.Optional(CONF_RANDOM, default=False): cv.boolean,
|
vol.Optional(CONF_RANDOM, default=False): cv.boolean,
|
||||||
vol.Required(CONF_NUM_LEDS): vol.All(cv.uint32_t, vol.Range(min=1)),
|
vol.Required(CONF_NUM_LEDS): vol.All(cv.uint32_t, vol.Range(min=1)),
|
||||||
})]),
|
}),
|
||||||
vol.Optional(CONF_ADD_LED_INTERVAL): cv.positive_time_period_milliseconds,
|
vol.Optional(CONF_ADD_LED_INTERVAL): cv.positive_time_period_milliseconds,
|
||||||
vol.Optional(CONF_REVERSE): cv.boolean,
|
vol.Optional(CONF_REVERSE): cv.boolean,
|
||||||
}),
|
}),
|
||||||
|
@ -178,7 +178,8 @@ EFFECTS_SCHEMA = vol.Schema({
|
||||||
def validate_effects(allowed_effects):
|
def validate_effects(allowed_effects):
|
||||||
def validator(value):
|
def validator(value):
|
||||||
is_list = isinstance(value, list)
|
is_list = isinstance(value, list)
|
||||||
value = cv.ensure_list(value)
|
if not is_list:
|
||||||
|
value = [value]
|
||||||
names = set()
|
names = set()
|
||||||
ret = []
|
ret = []
|
||||||
for i, effect in enumerate(value):
|
for i, effect in enumerate(value):
|
||||||
|
@ -471,10 +472,10 @@ def light_turn_on_to_code(config, action_id, arg_type, template_arg):
|
||||||
|
|
||||||
def core_to_hass_config(data, config, brightness=True, rgb=True, color_temp=True,
|
def core_to_hass_config(data, config, brightness=True, rgb=True, color_temp=True,
|
||||||
white_value=True):
|
white_value=True):
|
||||||
ret = mqtt.build_hass_config(data, 'light', config, include_state=True, include_command=True,
|
ret = mqtt.build_hass_config(data, 'light', config, include_state=True, include_command=True)
|
||||||
platform='mqtt_json')
|
|
||||||
if ret is None:
|
if ret is None:
|
||||||
return None
|
return None
|
||||||
|
ret['schema'] = 'json'
|
||||||
if brightness:
|
if brightness:
|
||||||
ret['brightness'] = True
|
ret['brightness'] = True
|
||||||
if rgb:
|
if rgb:
|
||||||
|
|
|
@ -108,7 +108,7 @@ def validate_printf(value):
|
||||||
CONF_LOGGER_LOG = 'logger.log'
|
CONF_LOGGER_LOG = 'logger.log'
|
||||||
LOGGER_LOG_ACTION_SCHEMA = vol.All(maybe_simple_message({
|
LOGGER_LOG_ACTION_SCHEMA = vol.All(maybe_simple_message({
|
||||||
vol.Required(CONF_FORMAT): cv.string,
|
vol.Required(CONF_FORMAT): cv.string,
|
||||||
vol.Optional(CONF_ARGS, default=list): vol.All(cv.ensure_list, [cv.lambda_]),
|
vol.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_),
|
||||||
vol.Optional(CONF_LEVEL, default="DEBUG"): cv.one_of(*LOG_LEVEL_TO_ESP_LOG, upper=True),
|
vol.Optional(CONF_LEVEL, default="DEBUG"): cv.one_of(*LOG_LEVEL_TO_ESP_LOG, upper=True),
|
||||||
vol.Optional(CONF_TAG, default="main"): cv.string,
|
vol.Optional(CONF_TAG, default="main"): cv.string,
|
||||||
}), validate_printf)
|
}), validate_printf)
|
||||||
|
|
|
@ -85,7 +85,7 @@ CONFIG_SCHEMA = vol.All(vol.Schema({
|
||||||
vol.Optional(CONF_LEVEL): logger.is_log_level,
|
vol.Optional(CONF_LEVEL): logger.is_log_level,
|
||||||
}), validate_message_just_topic),
|
}), validate_message_just_topic),
|
||||||
vol.Optional(CONF_SSL_FINGERPRINTS): vol.All(cv.only_on_esp8266,
|
vol.Optional(CONF_SSL_FINGERPRINTS): vol.All(cv.only_on_esp8266,
|
||||||
cv.ensure_list, [validate_fingerprint]),
|
cv.ensure_list(validate_fingerprint)),
|
||||||
vol.Optional(CONF_KEEPALIVE): cv.positive_time_period_seconds,
|
vol.Optional(CONF_KEEPALIVE): cv.positive_time_period_seconds,
|
||||||
vol.Optional(CONF_REBOOT_TIMEOUT): cv.positive_time_period_milliseconds,
|
vol.Optional(CONF_REBOOT_TIMEOUT): cv.positive_time_period_milliseconds,
|
||||||
vol.Optional(CONF_ON_MESSAGE): automation.validate_automation({
|
vol.Optional(CONF_ON_MESSAGE): automation.validate_automation({
|
||||||
|
@ -260,12 +260,11 @@ def get_default_topic_for(data, component_type, name, suffix):
|
||||||
sanitized_name, suffix)
|
sanitized_name, suffix)
|
||||||
|
|
||||||
|
|
||||||
def build_hass_config(data, component_type, config, include_state=True, include_command=True,
|
def build_hass_config(data, component_type, config, include_state=True, include_command=True):
|
||||||
platform='mqtt'):
|
|
||||||
if config.get(CONF_INTERNAL, False):
|
if config.get(CONF_INTERNAL, False):
|
||||||
return None
|
return None
|
||||||
ret = OrderedDict()
|
ret = OrderedDict()
|
||||||
ret['platform'] = platform
|
ret['platform'] = 'mqtt'
|
||||||
ret['name'] = config[CONF_NAME]
|
ret['name'] = config[CONF_NAME]
|
||||||
if include_state:
|
if include_state:
|
||||||
default = get_default_topic_for(data, component_type, config[CONF_NAME], 'state')
|
default = get_default_topic_for(data, component_type, config[CONF_NAME], 'state')
|
||||||
|
|
|
@ -13,18 +13,18 @@ BINARY_SCHEMA = output.PLATFORM_SCHEMA.extend({
|
||||||
cv.GenerateID(): cv.declare_variable_id(CustomBinaryOutputConstructor),
|
cv.GenerateID(): cv.declare_variable_id(CustomBinaryOutputConstructor),
|
||||||
vol.Required(CONF_LAMBDA): cv.lambda_,
|
vol.Required(CONF_LAMBDA): cv.lambda_,
|
||||||
vol.Required(CONF_OUTPUTS):
|
vol.Required(CONF_OUTPUTS):
|
||||||
vol.All(cv.ensure_list, [output.BINARY_OUTPUT_SCHEMA.extend({
|
cv.ensure_list(output.BINARY_OUTPUT_SCHEMA.extend({
|
||||||
cv.GenerateID(): cv.declare_variable_id(output.BinaryOutput),
|
cv.GenerateID(): cv.declare_variable_id(output.BinaryOutput),
|
||||||
})]),
|
})),
|
||||||
})
|
})
|
||||||
|
|
||||||
FLOAT_SCHEMA = output.PLATFORM_SCHEMA.extend({
|
FLOAT_SCHEMA = output.PLATFORM_SCHEMA.extend({
|
||||||
cv.GenerateID(): cv.declare_variable_id(CustomFloatOutputConstructor),
|
cv.GenerateID(): cv.declare_variable_id(CustomFloatOutputConstructor),
|
||||||
vol.Required(CONF_LAMBDA): cv.lambda_,
|
vol.Required(CONF_LAMBDA): cv.lambda_,
|
||||||
vol.Required(CONF_OUTPUTS):
|
vol.Required(CONF_OUTPUTS):
|
||||||
vol.All(cv.ensure_list, [output.FLOAT_OUTPUT_PLATFORM_SCHEMA.extend({
|
cv.ensure_list(output.FLOAT_OUTPUT_PLATFORM_SCHEMA.extend({
|
||||||
cv.GenerateID(): cv.declare_variable_id(output.FloatOutput),
|
cv.GenerateID(): cv.declare_variable_id(output.FloatOutput),
|
||||||
})]),
|
})),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -41,8 +41,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||||
cv.GenerateID(): cv.declare_variable_id(RemoteReceiverComponent),
|
cv.GenerateID(): cv.declare_variable_id(RemoteReceiverComponent),
|
||||||
vol.Required(CONF_PIN): pins.gpio_input_pin_schema,
|
vol.Required(CONF_PIN): pins.gpio_input_pin_schema,
|
||||||
vol.Optional(CONF_DUMP, default=[]):
|
vol.Optional(CONF_DUMP, default=[]):
|
||||||
vol.Any(validate_dumpers_all,
|
vol.Any(validate_dumpers_all, cv.ensure_list(cv.one_of(*DUMPERS, lower=True))),
|
||||||
vol.All(cv.ensure_list, [cv.one_of(*DUMPERS, lower=True)])),
|
|
||||||
vol.Optional(CONF_TOLERANCE): vol.All(cv.percentage_int, vol.Range(min=0)),
|
vol.Optional(CONF_TOLERANCE): vol.All(cv.percentage_int, vol.Range(min=0)),
|
||||||
vol.Optional(CONF_BUFFER_SIZE): cv.validate_bytes,
|
vol.Optional(CONF_BUFFER_SIZE): cv.validate_bytes,
|
||||||
vol.Optional(CONF_FILTER): cv.positive_time_period_microseconds,
|
vol.Optional(CONF_FILTER): cv.positive_time_period_microseconds,
|
||||||
|
|
|
@ -40,7 +40,7 @@ FILTER_KEYS = [CONF_OFFSET, CONF_MULTIPLY, CONF_FILTER_OUT, CONF_FILTER_NAN,
|
||||||
CONF_SLIDING_WINDOW_MOVING_AVERAGE, CONF_EXPONENTIAL_MOVING_AVERAGE, CONF_LAMBDA,
|
CONF_SLIDING_WINDOW_MOVING_AVERAGE, CONF_EXPONENTIAL_MOVING_AVERAGE, CONF_LAMBDA,
|
||||||
CONF_THROTTLE, CONF_DELTA, CONF_UNIQUE, CONF_HEARTBEAT, CONF_DEBOUNCE, CONF_OR]
|
CONF_THROTTLE, CONF_DELTA, CONF_UNIQUE, CONF_HEARTBEAT, CONF_DEBOUNCE, CONF_OR]
|
||||||
|
|
||||||
FILTERS_SCHEMA = vol.All(cv.ensure_list, [vol.All({
|
FILTERS_SCHEMA = cv.ensure_list({
|
||||||
vol.Optional(CONF_OFFSET): cv.float_,
|
vol.Optional(CONF_OFFSET): cv.float_,
|
||||||
vol.Optional(CONF_MULTIPLY): cv.float_,
|
vol.Optional(CONF_MULTIPLY): cv.float_,
|
||||||
vol.Optional(CONF_FILTER_OUT): cv.float_,
|
vol.Optional(CONF_FILTER_OUT): cv.float_,
|
||||||
|
@ -61,7 +61,7 @@ FILTERS_SCHEMA = vol.All(cv.ensure_list, [vol.All({
|
||||||
vol.Optional(CONF_HEARTBEAT): cv.positive_time_period_milliseconds,
|
vol.Optional(CONF_HEARTBEAT): cv.positive_time_period_milliseconds,
|
||||||
vol.Optional(CONF_DEBOUNCE): cv.positive_time_period_milliseconds,
|
vol.Optional(CONF_DEBOUNCE): cv.positive_time_period_milliseconds,
|
||||||
vol.Optional(CONF_OR): validate_recursive_filter,
|
vol.Optional(CONF_OR): validate_recursive_filter,
|
||||||
}, cv.has_exactly_one_key(*FILTER_KEYS))])
|
}, cv.has_exactly_one_key(*FILTER_KEYS))
|
||||||
|
|
||||||
# Base
|
# Base
|
||||||
sensor_ns = esphomelib_ns.namespace('sensor')
|
sensor_ns = esphomelib_ns.namespace('sensor')
|
||||||
|
|
|
@ -11,9 +11,9 @@ CustomSensorConstructor = sensor.sensor_ns.class_('CustomSensorConstructor')
|
||||||
PLATFORM_SCHEMA = sensor.PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = sensor.PLATFORM_SCHEMA.extend({
|
||||||
cv.GenerateID(): cv.declare_variable_id(CustomSensorConstructor),
|
cv.GenerateID(): cv.declare_variable_id(CustomSensorConstructor),
|
||||||
vol.Required(CONF_LAMBDA): cv.lambda_,
|
vol.Required(CONF_LAMBDA): cv.lambda_,
|
||||||
vol.Required(CONF_SENSORS): vol.All(cv.ensure_list, [sensor.SENSOR_SCHEMA.extend({
|
vol.Required(CONF_SENSORS): cv.ensure_list(sensor.SENSOR_SCHEMA.extend({
|
||||||
cv.GenerateID(): cv.declare_variable_id(sensor.Sensor),
|
cv.GenerateID(): cv.declare_variable_id(sensor.Sensor),
|
||||||
})]),
|
})),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
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),
|
cv.GenerateID(): cv.declare_variable_id(CustomSwitchConstructor),
|
||||||
vol.Required(CONF_LAMBDA): cv.lambda_,
|
vol.Required(CONF_LAMBDA): cv.lambda_,
|
||||||
vol.Required(CONF_SWITCHES):
|
vol.Required(CONF_SWITCHES):
|
||||||
vol.All(cv.ensure_list, [switch.SWITCH_SCHEMA.extend({
|
cv.ensure_list(switch.SWITCH_SCHEMA.extend({
|
||||||
cv.GenerateID(): cv.declare_variable_id(switch.Switch),
|
cv.GenerateID(): cv.declare_variable_id(switch.Switch),
|
||||||
})]),
|
})),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,9 @@ PLATFORM_SCHEMA = text_sensor.PLATFORM_SCHEMA.extend({
|
||||||
cv.GenerateID(): cv.declare_variable_id(CustomTextSensorConstructor),
|
cv.GenerateID(): cv.declare_variable_id(CustomTextSensorConstructor),
|
||||||
vol.Required(CONF_LAMBDA): cv.lambda_,
|
vol.Required(CONF_LAMBDA): cv.lambda_,
|
||||||
vol.Required(CONF_TEXT_SENSORS):
|
vol.Required(CONF_TEXT_SENSORS):
|
||||||
vol.All(cv.ensure_list, [text_sensor.TEXT_SENSOR_SCHEMA.extend({
|
cv.ensure_list(text_sensor.TEXT_SENSOR_SCHEMA.extend({
|
||||||
cv.GenerateID(): cv.declare_variable_id(text_sensor.TextSensor),
|
cv.GenerateID(): cv.declare_variable_id(text_sensor.TextSensor),
|
||||||
})]),
|
})),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
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 voluptuous as vol
|
||||||
|
|
||||||
import esphomeyaml.config_validation as cv
|
|
||||||
from esphomeyaml.components import time as time_
|
from esphomeyaml.components import time as time_
|
||||||
from esphomeyaml.const import CONF_ID, CONF_LAMBDA, CONF_SERVERS
|
import esphomeyaml.config_validation as cv
|
||||||
|
from esphomeyaml.const import CONF_ID, CONF_SERVERS
|
||||||
from esphomeyaml.cpp_generator import Pvariable, add
|
from esphomeyaml.cpp_generator import Pvariable, add
|
||||||
from esphomeyaml.cpp_helpers import setup_component
|
from esphomeyaml.cpp_helpers import setup_component
|
||||||
from esphomeyaml.cpp_types import App
|
from esphomeyaml.cpp_types import App
|
||||||
|
@ -11,8 +11,7 @@ SNTPComponent = time_.time_ns.class_('SNTPComponent', time_.RealTimeClockCompone
|
||||||
|
|
||||||
PLATFORM_SCHEMA = time_.TIME_PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = time_.TIME_PLATFORM_SCHEMA.extend({
|
||||||
cv.GenerateID(): cv.declare_variable_id(SNTPComponent),
|
cv.GenerateID(): cv.declare_variable_id(SNTPComponent),
|
||||||
vol.Optional(CONF_SERVERS): vol.All(cv.ensure_list, [cv.domain], vol.Length(min=1, max=3)),
|
vol.Optional(CONF_SERVERS): vol.All(cv.ensure_list(cv.domain), vol.Length(min=1, max=3)),
|
||||||
vol.Optional(CONF_LAMBDA): cv.lambda_,
|
|
||||||
}).extend(cv.COMPONENT_SCHEMA.schema)
|
}).extend(cv.COMPONENT_SCHEMA.schema)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,25 @@ import voluptuous as vol
|
||||||
import esphomeyaml.config_validation as cv
|
import esphomeyaml.config_validation as cv
|
||||||
from esphomeyaml.const import CONF_AP, CONF_CHANNEL, CONF_DNS1, CONF_DNS2, CONF_DOMAIN, \
|
from esphomeyaml.const import CONF_AP, CONF_CHANNEL, CONF_DNS1, CONF_DNS2, CONF_DOMAIN, \
|
||||||
CONF_GATEWAY, CONF_HOSTNAME, CONF_ID, CONF_MANUAL_IP, CONF_PASSWORD, CONF_POWER_SAVE_MODE, \
|
CONF_GATEWAY, CONF_HOSTNAME, CONF_ID, CONF_MANUAL_IP, CONF_PASSWORD, CONF_POWER_SAVE_MODE, \
|
||||||
CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, CONF_SUBNET
|
CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, CONF_SUBNET, CONF_NETWORKS, CONF_BSSID
|
||||||
from esphomeyaml.core import CORE
|
from esphomeyaml.core import CORE, HexInt
|
||||||
from esphomeyaml.cpp_generator import Pvariable, StructInitializer, add
|
from esphomeyaml.cpp_generator import Pvariable, StructInitializer, add, variable, ArrayInitializer
|
||||||
from esphomeyaml.cpp_types import App, Component, esphomelib_ns, global_ns
|
from esphomeyaml.cpp_types import App, Component, esphomelib_ns, global_ns
|
||||||
|
|
||||||
|
|
||||||
|
IPAddress = global_ns.class_('IPAddress')
|
||||||
|
ManualIP = esphomelib_ns.struct('ManualIP')
|
||||||
|
WiFiComponent = esphomelib_ns.class_('WiFiComponent', Component)
|
||||||
|
WiFiAP = esphomelib_ns.struct('WiFiAP')
|
||||||
|
|
||||||
|
WiFiPowerSaveMode = esphomelib_ns.enum('WiFiPowerSaveMode')
|
||||||
|
WIFI_POWER_SAVE_MODES = {
|
||||||
|
'NONE': WiFiPowerSaveMode.WIFI_POWER_SAVE_NONE,
|
||||||
|
'LIGHT': WiFiPowerSaveMode.WIFI_POWER_SAVE_LIGHT,
|
||||||
|
'HIGH': WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def validate_password(value):
|
def validate_password(value):
|
||||||
value = cv.string(value)
|
value = cv.string(value)
|
||||||
if not value:
|
if not value:
|
||||||
|
@ -41,10 +54,11 @@ STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend({
|
||||||
})
|
})
|
||||||
|
|
||||||
WIFI_NETWORK_BASE = vol.Schema({
|
WIFI_NETWORK_BASE = vol.Schema({
|
||||||
vol.Required(CONF_SSID): cv.ssid,
|
cv.GenerateID(): cv.declare_variable_id(WiFiAP),
|
||||||
|
vol.Optional(CONF_SSID): cv.ssid,
|
||||||
vol.Optional(CONF_PASSWORD): validate_password,
|
vol.Optional(CONF_PASSWORD): validate_password,
|
||||||
vol.Optional(CONF_CHANNEL): validate_channel,
|
vol.Optional(CONF_CHANNEL): validate_channel,
|
||||||
vol.Optional(CONF_MANUAL_IP): AP_MANUAL_IP_SCHEMA,
|
vol.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA,
|
||||||
})
|
})
|
||||||
|
|
||||||
WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend({
|
WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend({
|
||||||
|
@ -53,35 +67,39 @@ WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend({
|
||||||
|
|
||||||
WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend({
|
WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend({
|
||||||
vol.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA,
|
vol.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA,
|
||||||
|
vol.Optional(CONF_BSSID): cv.mac_address,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def validate(config):
|
def validate(config):
|
||||||
if CONF_PASSWORD in config and CONF_SSID not in config:
|
if CONF_PASSWORD in config and CONF_SSID not in config:
|
||||||
raise vol.Invalid("Cannot have WiFi password without SSID!")
|
raise vol.Invalid("Cannot have WiFi password without SSID!")
|
||||||
if (CONF_SSID not in config) and (CONF_AP not in config):
|
|
||||||
|
if CONF_SSID in config:
|
||||||
|
network = {CONF_SSID: config.pop(CONF_SSID)}
|
||||||
|
if CONF_PASSWORD in config:
|
||||||
|
network[CONF_PASSWORD] = config.pop(CONF_PASSWORD)
|
||||||
|
if CONF_MANUAL_IP in config:
|
||||||
|
network[CONF_MANUAL_IP] = config.pop(CONF_MANUAL_IP)
|
||||||
|
if CONF_NETWORKS in config:
|
||||||
|
raise vol.Invalid("You cannot use the 'ssid:' option together with 'networks:'. Please "
|
||||||
|
"copy your network into the 'networks:' key")
|
||||||
|
config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network)
|
||||||
|
|
||||||
|
if (CONF_NETWORKS not in config) and (CONF_AP not in config):
|
||||||
raise vol.Invalid("Please specify at least an SSID or an Access Point "
|
raise vol.Invalid("Please specify at least an SSID or an Access Point "
|
||||||
"to create.")
|
"to create.")
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
IPAddress = global_ns.class_('IPAddress')
|
|
||||||
ManualIP = esphomelib_ns.struct('ManualIP')
|
|
||||||
WiFiComponent = esphomelib_ns.class_('WiFiComponent', Component)
|
|
||||||
WiFiAp = esphomelib_ns.struct('WiFiAp')
|
|
||||||
|
|
||||||
WiFiPowerSaveMode = esphomelib_ns.enum('WiFiPowerSaveMode')
|
|
||||||
WIFI_POWER_SAVE_MODES = {
|
|
||||||
'NONE': WiFiPowerSaveMode.WIFI_POWER_SAVE_NONE,
|
|
||||||
'LIGHT': WiFiPowerSaveMode.WIFI_POWER_SAVE_LIGHT,
|
|
||||||
'HIGH': WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH,
|
|
||||||
}
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.All(vol.Schema({
|
CONFIG_SCHEMA = vol.All(vol.Schema({
|
||||||
cv.GenerateID(): cv.declare_variable_id(WiFiComponent),
|
cv.GenerateID(): cv.declare_variable_id(WiFiComponent),
|
||||||
|
vol.Optional(CONF_NETWORKS): cv.ensure_list(WIFI_NETWORK_STA),
|
||||||
|
|
||||||
vol.Optional(CONF_SSID): cv.ssid,
|
vol.Optional(CONF_SSID): cv.ssid,
|
||||||
vol.Optional(CONF_PASSWORD): validate_password,
|
vol.Optional(CONF_PASSWORD): validate_password,
|
||||||
vol.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA,
|
vol.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA,
|
||||||
|
|
||||||
vol.Optional(CONF_AP): WIFI_NETWORK_AP,
|
vol.Optional(CONF_AP): WIFI_NETWORK_AP,
|
||||||
vol.Optional(CONF_HOSTNAME): cv.hostname,
|
vol.Optional(CONF_HOSTNAME): cv.hostname,
|
||||||
vol.Optional(CONF_DOMAIN, default='.local'): cv.domain_name,
|
vol.Optional(CONF_DOMAIN, default='.local'): cv.domain_name,
|
||||||
|
@ -110,21 +128,28 @@ def manual_ip(config):
|
||||||
|
|
||||||
|
|
||||||
def wifi_network(config):
|
def wifi_network(config):
|
||||||
return StructInitializer(
|
ap = variable(config[CONF_ID], WiFiAP())
|
||||||
WiFiAp,
|
if CONF_SSID in config:
|
||||||
('ssid', config.get(CONF_SSID, "")),
|
add(ap.set_ssid(config[CONF_SSID]))
|
||||||
('password', config.get(CONF_PASSWORD, "")),
|
if CONF_PASSWORD in config:
|
||||||
('channel', config.get(CONF_CHANNEL, -1)),
|
add(ap.set_password(config[CONF_PASSWORD]))
|
||||||
('manual_ip', manual_ip(config.get(CONF_MANUAL_IP))),
|
if CONF_BSSID in config:
|
||||||
)
|
bssid = [HexInt(i) for i in config[CONF_BSSID].parts]
|
||||||
|
add(ap.set_bssid(ArrayInitializer(*bssid, multiline=False)))
|
||||||
|
if CONF_CHANNEL in config:
|
||||||
|
add(ap.set_channel(config[CONF_CHANNEL]))
|
||||||
|
if CONF_MANUAL_IP in config:
|
||||||
|
add(ap.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))
|
||||||
|
|
||||||
|
return ap
|
||||||
|
|
||||||
|
|
||||||
def to_code(config):
|
def to_code(config):
|
||||||
rhs = App.init_wifi()
|
rhs = App.init_wifi()
|
||||||
wifi = Pvariable(config[CONF_ID], rhs)
|
wifi = Pvariable(config[CONF_ID], rhs)
|
||||||
|
|
||||||
if CONF_SSID in config:
|
for network in config.get(CONF_NETWORKS, []):
|
||||||
add(wifi.set_sta(wifi_network(config)))
|
add(wifi.add_sta(wifi_network(network)))
|
||||||
|
|
||||||
if CONF_AP in config:
|
if CONF_AP in config:
|
||||||
add(wifi.set_ap(wifi_network(config[CONF_AP])))
|
add(wifi.set_ap(wifi_network(config[CONF_AP])))
|
||||||
|
|
|
@ -102,13 +102,25 @@ def boolean(value):
|
||||||
return bool(value)
|
return bool(value)
|
||||||
|
|
||||||
|
|
||||||
def ensure_list(value):
|
def ensure_list(*validators):
|
||||||
"""Wrap value in list if it is not one."""
|
"""Wrap value in list if it is not one."""
|
||||||
if value is None or (isinstance(value, dict) and not value):
|
user = vol.All(*validators)
|
||||||
return []
|
|
||||||
if isinstance(value, list):
|
def validator(value):
|
||||||
return value
|
if value is None or (isinstance(value, dict) and not value):
|
||||||
return [value]
|
return []
|
||||||
|
if not isinstance(value, list):
|
||||||
|
return [user(value)]
|
||||||
|
ret = []
|
||||||
|
for i, val in enumerate(value):
|
||||||
|
try:
|
||||||
|
ret.append(user(val))
|
||||||
|
except vol.Invalid as err:
|
||||||
|
err.prepend(i)
|
||||||
|
raise err
|
||||||
|
return ret
|
||||||
|
|
||||||
|
return validator
|
||||||
|
|
||||||
|
|
||||||
def ensure_list_not_empty(value):
|
def ensure_list_not_empty(value):
|
||||||
|
@ -469,16 +481,18 @@ def ssid(value):
|
||||||
raise vol.Invalid("SSID must be a string. Did you wrap it in quotes?")
|
raise vol.Invalid("SSID must be a string. Did you wrap it in quotes?")
|
||||||
if not value:
|
if not value:
|
||||||
raise vol.Invalid("SSID can't be empty.")
|
raise vol.Invalid("SSID can't be empty.")
|
||||||
if len(value) > 31:
|
if len(value) > 32:
|
||||||
raise vol.Invalid("SSID can't be longer than 31 characters")
|
raise vol.Invalid("SSID can't be longer than 32 characters")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def ipv4(value):
|
def ipv4(value):
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
parts = value
|
parts = value
|
||||||
elif isinstance(value, str):
|
elif isinstance(value, basestring):
|
||||||
parts = value.split('.')
|
parts = value.split('.')
|
||||||
|
elif isinstance(value, IPAddress):
|
||||||
|
return value
|
||||||
else:
|
else:
|
||||||
raise vol.Invalid("IPv4 address must consist of either string or "
|
raise vol.Invalid("IPv4 address must consist of either string or "
|
||||||
"integer list")
|
"integer list")
|
||||||
|
@ -664,6 +678,16 @@ def file_(value):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
ENTITY_ID_PATTERN = re.compile(r"^([a-z0-9]+)\.([a-z0-9]+)$")
|
||||||
|
|
||||||
|
|
||||||
|
def entity_id(value):
|
||||||
|
value = string_strict(value).lower()
|
||||||
|
if ENTITY_ID_PATTERN.match(value) is None:
|
||||||
|
raise vol.Invalid(u"Invalid entity ID: {}".format(value))
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class GenerateID(vol.Optional):
|
class GenerateID(vol.Optional):
|
||||||
def __init__(self, key=CONF_ID):
|
def __init__(self, key=CONF_ID):
|
||||||
super(GenerateID, self).__init__(key, default=lambda: None)
|
super(GenerateID, self).__init__(key, default=lambda: None)
|
||||||
|
|
|
@ -28,6 +28,7 @@ CONF_BRANCH = 'branch'
|
||||||
CONF_LOGGER = 'logger'
|
CONF_LOGGER = 'logger'
|
||||||
CONF_WIFI = 'wifi'
|
CONF_WIFI = 'wifi'
|
||||||
CONF_SSID = 'ssid'
|
CONF_SSID = 'ssid'
|
||||||
|
CONF_BSSID = 'bssid'
|
||||||
CONF_PASSWORD = 'password'
|
CONF_PASSWORD = 'password'
|
||||||
CONF_MANUAL_IP = 'manual_ip'
|
CONF_MANUAL_IP = 'manual_ip'
|
||||||
CONF_STATIC_IP = 'static_ip'
|
CONF_STATIC_IP = 'static_ip'
|
||||||
|
@ -222,6 +223,7 @@ CONF_ACCURACY = 'accuracy'
|
||||||
CONF_BOARD_FLASH_MODE = 'board_flash_mode'
|
CONF_BOARD_FLASH_MODE = 'board_flash_mode'
|
||||||
CONF_ON_PRESS = 'on_press'
|
CONF_ON_PRESS = 'on_press'
|
||||||
CONF_ON_RELEASE = 'on_release'
|
CONF_ON_RELEASE = 'on_release'
|
||||||
|
CONF_ON_STATE = 'on_state'
|
||||||
CONF_ON_CLICK = 'on_click'
|
CONF_ON_CLICK = 'on_click'
|
||||||
CONF_ON_DOUBLE_CLICK = 'on_double_click'
|
CONF_ON_DOUBLE_CLICK = 'on_double_click'
|
||||||
CONF_ON_MULTI_CLICK = 'on_multi_click'
|
CONF_ON_MULTI_CLICK = 'on_multi_click'
|
||||||
|
@ -386,6 +388,10 @@ CONF_PIN_D = 'pin_d'
|
||||||
CONF_SLEEP_WHEN_DONE = 'sleep_when_done'
|
CONF_SLEEP_WHEN_DONE = 'sleep_when_done'
|
||||||
CONF_STEP_MODE = 'step_mode'
|
CONF_STEP_MODE = 'step_mode'
|
||||||
CONF_COMPONENTS = 'components'
|
CONF_COMPONENTS = 'components'
|
||||||
|
CONF_DATA_TEMPLATE = 'data_template'
|
||||||
|
CONF_VARIABLES = 'variables'
|
||||||
|
CONF_SERVICE = 'service'
|
||||||
|
CONF_ENTITY_ID = 'entity_id'
|
||||||
|
|
||||||
ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_'
|
ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_'
|
||||||
ARDUINO_VERSION_ESP32_DEV = 'https://github.com/platformio/platform-espressif32.git#feature/stage'
|
ARDUINO_VERSION_ESP32_DEV = 'https://github.com/platformio/platform-espressif32.git#feature/stage'
|
||||||
|
|
|
@ -282,6 +282,8 @@ class ID(object):
|
||||||
# pylint: disable=too-many-instance-attributes
|
# pylint: disable=too-many-instance-attributes
|
||||||
class EsphomeyamlCore(object):
|
class EsphomeyamlCore(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
# True if command is run from dashboard
|
||||||
|
self.dashboard = False
|
||||||
# The name of the node
|
# The name of the node
|
||||||
self.name = None # type: str
|
self.name = None # type: str
|
||||||
# The relative path to the configuration YAML
|
# The relative path to the configuration YAML
|
||||||
|
|
|
@ -175,8 +175,8 @@ CONFIG_SCHEMA = vol.Schema({
|
||||||
vol.Optional(CONF_ON_LOOP): automation.validate_automation({
|
vol.Optional(CONF_ON_LOOP): automation.validate_automation({
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(LoopTrigger),
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(LoopTrigger),
|
||||||
}),
|
}),
|
||||||
vol.Optional(CONF_INCLUDES): vol.All(cv.ensure_list, [cv.file_]),
|
vol.Optional(CONF_INCLUDES): cv.ensure_list(cv.file_),
|
||||||
vol.Optional(CONF_LIBRARIES): vol.All(cv.ensure_list, [cv.string_strict]),
|
vol.Optional(CONF_LIBRARIES): cv.ensure_list(cv.string_strict),
|
||||||
|
|
||||||
vol.Optional('library_uri'): cv.invalid("The library_uri option has been removed in 1.8.0 and "
|
vol.Optional('library_uri'): cv.invalid("The library_uri option has been removed in 1.8.0 and "
|
||||||
"was moved into the esphomelib_version option."),
|
"was moved into the esphomelib_version option."),
|
||||||
|
|
|
@ -103,49 +103,49 @@ class EsphomeyamlLogsHandler(EsphomeyamlCommandWebSocket):
|
||||||
def build_command(self, message):
|
def build_command(self, message):
|
||||||
js = json.loads(message)
|
js = json.loads(message)
|
||||||
config_file = CONFIG_DIR + '/' + js['configuration']
|
config_file = CONFIG_DIR + '/' + js['configuration']
|
||||||
return ["esphomeyaml", config_file, "logs", '--serial-port', js["port"]]
|
return ["esphomeyaml", "--dashboard", config_file, "logs", '--serial-port', js["port"]]
|
||||||
|
|
||||||
|
|
||||||
class EsphomeyamlRunHandler(EsphomeyamlCommandWebSocket):
|
class EsphomeyamlRunHandler(EsphomeyamlCommandWebSocket):
|
||||||
def build_command(self, message):
|
def build_command(self, message):
|
||||||
js = json.loads(message)
|
js = json.loads(message)
|
||||||
config_file = os.path.join(CONFIG_DIR, js['configuration'])
|
config_file = os.path.join(CONFIG_DIR, js['configuration'])
|
||||||
return ["esphomeyaml", config_file, "run", '--upload-port', js["port"]]
|
return ["esphomeyaml", "--dashboard", config_file, "run", '--upload-port', js["port"]]
|
||||||
|
|
||||||
|
|
||||||
class EsphomeyamlCompileHandler(EsphomeyamlCommandWebSocket):
|
class EsphomeyamlCompileHandler(EsphomeyamlCommandWebSocket):
|
||||||
def build_command(self, message):
|
def build_command(self, message):
|
||||||
js = json.loads(message)
|
js = json.loads(message)
|
||||||
config_file = os.path.join(CONFIG_DIR, js['configuration'])
|
config_file = os.path.join(CONFIG_DIR, js['configuration'])
|
||||||
return ["esphomeyaml", config_file, "compile"]
|
return ["esphomeyaml", "--dashboard", config_file, "compile"]
|
||||||
|
|
||||||
|
|
||||||
class EsphomeyamlValidateHandler(EsphomeyamlCommandWebSocket):
|
class EsphomeyamlValidateHandler(EsphomeyamlCommandWebSocket):
|
||||||
def build_command(self, message):
|
def build_command(self, message):
|
||||||
js = json.loads(message)
|
js = json.loads(message)
|
||||||
config_file = os.path.join(CONFIG_DIR, js['configuration'])
|
config_file = os.path.join(CONFIG_DIR, js['configuration'])
|
||||||
return ["esphomeyaml", config_file, "config"]
|
return ["esphomeyaml", "--dashboard", config_file, "config"]
|
||||||
|
|
||||||
|
|
||||||
class EsphomeyamlCleanMqttHandler(EsphomeyamlCommandWebSocket):
|
class EsphomeyamlCleanMqttHandler(EsphomeyamlCommandWebSocket):
|
||||||
def build_command(self, message):
|
def build_command(self, message):
|
||||||
js = json.loads(message)
|
js = json.loads(message)
|
||||||
config_file = os.path.join(CONFIG_DIR, js['configuration'])
|
config_file = os.path.join(CONFIG_DIR, js['configuration'])
|
||||||
return ["esphomeyaml", config_file, "clean-mqtt"]
|
return ["esphomeyaml", "--dashboard", config_file, "clean-mqtt"]
|
||||||
|
|
||||||
|
|
||||||
class EsphomeyamlCleanHandler(EsphomeyamlCommandWebSocket):
|
class EsphomeyamlCleanHandler(EsphomeyamlCommandWebSocket):
|
||||||
def build_command(self, message):
|
def build_command(self, message):
|
||||||
js = json.loads(message)
|
js = json.loads(message)
|
||||||
config_file = os.path.join(CONFIG_DIR, js['configuration'])
|
config_file = os.path.join(CONFIG_DIR, js['configuration'])
|
||||||
return ["esphomeyaml", config_file, "clean"]
|
return ["esphomeyaml", "--dashboard", config_file, "clean"]
|
||||||
|
|
||||||
|
|
||||||
class EsphomeyamlHassConfigHandler(EsphomeyamlCommandWebSocket):
|
class EsphomeyamlHassConfigHandler(EsphomeyamlCommandWebSocket):
|
||||||
def build_command(self, message):
|
def build_command(self, message):
|
||||||
js = json.loads(message)
|
js = json.loads(message)
|
||||||
config_file = os.path.join(CONFIG_DIR, js['configuration'])
|
config_file = os.path.join(CONFIG_DIR, js['configuration'])
|
||||||
return ["esphomeyaml", config_file, "hass-config"]
|
return ["esphomeyaml", "--dashboard", config_file, "hass-config"]
|
||||||
|
|
||||||
|
|
||||||
class SerialPortRequestHandler(BaseHandler):
|
class SerialPortRequestHandler(BaseHandler):
|
||||||
|
@ -294,10 +294,9 @@ class MainRequestHandler(BaseHandler):
|
||||||
version = const.__version__
|
version = const.__version__
|
||||||
docs_link = 'https://beta.esphomelib.com/esphomeyaml/' if 'b' in version else \
|
docs_link = 'https://beta.esphomelib.com/esphomeyaml/' if 'b' in version else \
|
||||||
'https://esphomelib.com/esphomeyaml/'
|
'https://esphomelib.com/esphomeyaml/'
|
||||||
mqtt_config = get_mqtt_config_lazy()
|
|
||||||
|
|
||||||
self.render("templates/index.html", entries=entries,
|
self.render("templates/index.html", entries=entries,
|
||||||
version=version, begin=begin, docs_link=docs_link, mqtt_config=mqtt_config)
|
version=version, begin=begin, docs_link=docs_link)
|
||||||
|
|
||||||
|
|
||||||
def _ping_func(filename, address):
|
def _ping_func(filename, address):
|
||||||
|
@ -497,43 +496,6 @@ def make_app(debug=False):
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
def _get_mqtt_config_impl():
|
|
||||||
import requests
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'X-HASSIO-KEY': os.getenv('HASSIO_TOKEN'),
|
|
||||||
}
|
|
||||||
|
|
||||||
mqtt_config = requests.get('http://hassio/services/mqtt', headers=headers).json()['data']
|
|
||||||
info = requests.get('http://hassio/host/info', headers=headers).json()['data']
|
|
||||||
host = '{}.local'.format(info['hostname'])
|
|
||||||
port = mqtt_config['port']
|
|
||||||
if port != 1883:
|
|
||||||
host = '{}:{}'.format(host, port)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'ssl': mqtt_config['ssl'],
|
|
||||||
'host': host,
|
|
||||||
'username': mqtt_config.get('username', ''),
|
|
||||||
'password': mqtt_config.get('password', '')
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_mqtt_config_lazy():
|
|
||||||
global HASSIO_MQTT_CONFIG
|
|
||||||
|
|
||||||
if not ON_HASSIO:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if HASSIO_MQTT_CONFIG is None:
|
|
||||||
try:
|
|
||||||
HASSIO_MQTT_CONFIG = _get_mqtt_config_impl()
|
|
||||||
except Exception: # pylint: disable=broad-except
|
|
||||||
pass
|
|
||||||
|
|
||||||
return HASSIO_MQTT_CONFIG
|
|
||||||
|
|
||||||
|
|
||||||
def start_web_server(args):
|
def start_web_server(args):
|
||||||
global CONFIG_DIR
|
global CONFIG_DIR
|
||||||
global PASSWORD_DIGEST
|
global PASSWORD_DIGEST
|
||||||
|
|
|
@ -15,7 +15,7 @@ const initializeColorState = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const colorReplace = (pre, state, text) => {
|
const colorReplace = (pre, state, text) => {
|
||||||
const re = /\033(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
|
const re = /(?:\033|\\033)(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
if (state.carriageReturn) {
|
if (state.carriageReturn) {
|
||||||
|
@ -176,7 +176,7 @@ const fetchPing = () => {
|
||||||
|
|
||||||
fetch('/ping', {credentials: "same-origin"}).then(res => res.json())
|
fetch('/ping', {credentials: "same-origin"}).then(res => res.json())
|
||||||
.then(response => {
|
.then(response => {
|
||||||
for (let filename of response) {
|
for (let filename in response) {
|
||||||
let node = document.querySelector(`.status-indicator[data-node="${filename}"]`);
|
let node = document.querySelector(`.status-indicator[data-node="${filename}"]`);
|
||||||
if (node === null)
|
if (node === null)
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>esphomeyaml Dashboard</title>
|
<title>ESPHome Dashboard</title>
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/static/materialize.min.css?v=1">
|
<link rel="stylesheet" href="/static/materialize.min.css?v=1">
|
||||||
<link rel="stylesheet" href="/static/materialize-stepper.min.css?v=1">
|
<link rel="stylesheet" href="/static/materialize-stepper.min.css?v=1">
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
<header>
|
<header>
|
||||||
<nav>
|
<nav>
|
||||||
<div class="nav-wrapper indigo">
|
<div class="nav-wrapper indigo">
|
||||||
<a href="#" class="brand-logo left">esphomeyaml Dashboard</a>
|
<a href="#" class="brand-logo left">ESPHome Dashboard</a>
|
||||||
<div class="select-port-container right" id="select-port-target">
|
<div class="select-port-container right" id="select-port-target">
|
||||||
<select></select>
|
<select></select>
|
||||||
</div>
|
</div>
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
<div class="tap-target-content">
|
<div class="tap-target-content">
|
||||||
<h5>Select Upload Port</h5>
|
<h5>Select Upload Port</h5>
|
||||||
<p>
|
<p>
|
||||||
Here you can select where esphomeyaml will attempt to show logs and upload firmwares to.
|
Here you can select where ESPHome will attempt to show logs and upload firmwares to.
|
||||||
By default, this is "OTA", or Over-The-Air. Note that you might have to restart the Hass.io add-on
|
By default, this is "OTA", or Over-The-Air. Note that you might have to restart the Hass.io add-on
|
||||||
for new serial ports to be detected.
|
for new serial ports to be detected.
|
||||||
</p>
|
</p>
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
<li><a class="action-clean-mqtt" data-node="{{ entry.filename }}">Clean MQTT</a></li>
|
<li><a class="action-clean-mqtt" data-node="{{ entry.filename }}">Clean MQTT</a></li>
|
||||||
<li><a class="action-clean" data-node="{{ entry.filename }}">Clean Build</a></li>
|
<li><a class="action-clean" data-node="{{ entry.filename }}">Clean Build</a></li>
|
||||||
<li><a class="action-compile" data-node="{{ entry.filename }}">Compile</a></li>
|
<li><a class="action-compile" data-node="{{ entry.filename }}">Compile</a></li>
|
||||||
<li><a class="action-hass-config" data-node="{{ entry.filename }}">Home Assistant Configuration</a></li>
|
<li><a class="action-hass-config" data-node="{{ entry.filename }}">HASS MQTT Configuration</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -164,8 +164,8 @@
|
||||||
<div class="step-content">
|
<div class="step-content">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
Hi there! I'm the esphomeyaml setup wizard and will guide you through setting up
|
Hi there! I'm the ESPHome setup wizard and will guide you through setting up
|
||||||
your first ESP8266 or ESP32-powered device using esphomeyaml.
|
your first ESP8266 or ESP32-powered device using ESPHome.
|
||||||
</p>
|
</p>
|
||||||
<a href="https://www.espressif.com/en/products/hardware/esp8266ex/overview" target="_blank">ESP8266s</a> and
|
<a href="https://www.espressif.com/en/products/hardware/esp8266ex/overview" target="_blank">ESP8266s</a> and
|
||||||
their successors (the <a href="https://www.espressif.com/en/products/hardware/esp32/overview" target="_blank">ESP32s</a>)
|
their successors (the <a href="https://www.espressif.com/en/products/hardware/esp32/overview" target="_blank">ESP32s</a>)
|
||||||
|
@ -174,19 +174,19 @@
|
||||||
such as the <a href="https://esphomelib.com/esphomeyaml/devices/nodemcu_esp8266.html" target="_blank">NodeMCU</a>.
|
such as the <a href="https://esphomelib.com/esphomeyaml/devices/nodemcu_esp8266.html" target="_blank">NodeMCU</a>.
|
||||||
<p>
|
<p>
|
||||||
</p>
|
</p>
|
||||||
<a href="https://esphomelib.com/esphomeyaml/index.html" target="_blank">esphomeyaml</a>,
|
<a href="https://esphomelib.com/esphomeyaml/index.html" target="_blank">ESPHome</a>,
|
||||||
the tool you're using here, creates custom firmwares for these devices using YAML configuration
|
the tool you're using here, creates custom firmwares for these devices using YAML configuration
|
||||||
files (similar to the ones you might be used to with Home Assistant).
|
files (similar to the ones you might be used to with Home Assistant).
|
||||||
<p>
|
<p>
|
||||||
</p>
|
</p>
|
||||||
This wizard will create a basic YAML configuration file for your "node" (the microcontroller).
|
This wizard will create a basic YAML configuration file for your "node" (the microcontroller).
|
||||||
Later, you will be able to customize this file and add some of
|
Later, you will be able to customize this file and add some of ESPHome's
|
||||||
<a href="https://github.com/OttoWinter/esphomelib" target="_blank">esphomelib's</a>
|
|
||||||
many integrations.
|
many integrations.
|
||||||
<p>
|
<p>
|
||||||
<p>
|
<p>
|
||||||
First, I need to know what this node should be called. Choose this name wisely, changing this
|
First, I need to know what this node should be called. Choose this name wisely, it should be unique among
|
||||||
later makes Over-The-Air Update attempts difficult.
|
all your ESPs.
|
||||||
|
|
||||||
Names must be <strong>lowercase</strong> and <strong>must not contain spaces</strong> (allowed characters: <code class="inlinecode">a-z</code>,
|
Names must be <strong>lowercase</strong> and <strong>must not contain spaces</strong> (allowed characters: <code class="inlinecode">a-z</code>,
|
||||||
<code class="inlinecode">0-9</code> and <code class="inlinecode">_</code>)
|
<code class="inlinecode">0-9</code> and <code class="inlinecode">_</code>)
|
||||||
</p>
|
</p>
|
||||||
|
@ -321,73 +321,15 @@
|
||||||
<label for="wifi_password">WiFi Password</label>
|
<label for="wifi_password">WiFi Password</label>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
Esphomelib automatically sets up an Over-The-Air update server on the node
|
ESPHome automatically sets up an Over-The-Air update server on the node
|
||||||
so that you only need to flash a firmware via USB once.
|
so that you only need to flash a firmware via USB once. This password
|
||||||
|
is also used to connect to the ESP from Home Assistant.
|
||||||
|
|
||||||
Optionally, you can set a password for this upload process here:
|
Optionally, you can set a password for this upload process here:
|
||||||
</p>
|
</p>
|
||||||
<div class="input-field col s12">
|
<div class="input-field col s12">
|
||||||
<input id="ota_password" class="validate" name="ota_password" type="password">
|
<input id="password" class="validate" name="password" type="password">
|
||||||
<label for="ota_password">OTA Password</label>
|
<label for="password">Access Password</label>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="step-actions">
|
|
||||||
<button class="waves-effect waves-dark btn indigo next-step">CONTINUE</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li class="step">
|
|
||||||
<div class="step-title waves-effect">MQTT</div>
|
|
||||||
<div class="step-content">
|
|
||||||
<div class="row">
|
|
||||||
{% if mqtt_config is None %}
|
|
||||||
<p>
|
|
||||||
esphomelib connects to your Home Assistant instance via
|
|
||||||
<a href="https://www.home-assistant.io/docs/mqtt/">MQTT</a>.
|
|
||||||
If you haven't already, please set up
|
|
||||||
MQTT on your Home Assistant server, for example with the
|
|
||||||
<a href="https://www.home-assistant.io/addons/mosquitto/">Mosquitto Hass.io Add-on</a>.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
When you're done with that, please enter your MQTT broker here. For example
|
|
||||||
<code class="inlinecode">192.168.1.100</code>.
|
|
||||||
Please also specify the MQTT username and password you wish esphomelib to use
|
|
||||||
(leave them empty if you're not using any authentication).
|
|
||||||
</p>
|
|
||||||
{% else %}
|
|
||||||
<p>
|
|
||||||
esphomelib connects to your Home Assistant instance via
|
|
||||||
<a href="https://www.home-assistant.io/docs/mqtt/">MQTT</a>. In this section you will have
|
|
||||||
to tell esphomelib which MQTT "broker" to use.
|
|
||||||
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
It looks like you've already set up MQTT, the values below are taken from your Hass.io MQTT add-on.
|
|
||||||
Please confirm they are correct and press CONTINUE.
|
|
||||||
</p>
|
|
||||||
{% end %}
|
|
||||||
<div class="input-field col s12">
|
|
||||||
{% if mqtt_config is None %}
|
|
||||||
<input id="mqtt_broker" class="validate" type="text" name="broker" required>
|
|
||||||
{% else %}
|
|
||||||
<input id="mqtt_broker" class="validate" type="text" name="broker" value="{{ mqtt_config['host'] }}" required>
|
|
||||||
{% end %}
|
|
||||||
<label for="mqtt_broker">MQTT Broker</label>
|
|
||||||
</div>
|
|
||||||
<div class="input-field col s6">
|
|
||||||
{% if mqtt_config is None %}
|
|
||||||
<input id="mqtt_username" class="validate" type="text" name="mqtt_username">
|
|
||||||
{% else %}
|
|
||||||
<input id="mqtt_username" class="validate" type="text" name="mqtt_username" value="{{ mqtt_config['username'] }}">
|
|
||||||
{% end%}
|
|
||||||
<label for="mqtt_username">MQTT Username</label>
|
|
||||||
</div>
|
|
||||||
<div class="input-field col s6">
|
|
||||||
{% if mqtt_config is None %}
|
|
||||||
<input id="mqtt_password" class="validate" name="mqtt_password" type="password">
|
|
||||||
{% else %}
|
|
||||||
<input id="mqtt_password" class="validate" name="mqtt_password" type="password" value="{{ mqtt_config['password'] }}">
|
|
||||||
{% end %}
|
|
||||||
<label for="mqtt_password">MQTT Password</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="step-actions">
|
<div class="step-actions">
|
||||||
|
@ -399,7 +341,7 @@
|
||||||
<div class="step-title waves-effect">Done!</div>
|
<div class="step-title waves-effect">Done!</div>
|
||||||
<div class="step-content">
|
<div class="step-content">
|
||||||
<p>
|
<p>
|
||||||
Hooray! 🎉🎉🎉 You've successfully created your first esphomeyaml configuration file.
|
Hooray! 🎉🎉🎉 You've successfully created your first ESPHome configuration file.
|
||||||
When you click Submit, I will save this configuration file under
|
When you click Submit, I will save this configuration file under
|
||||||
<code class="inlinecode"><HASS_CONFIG_FOLDER>/esphomeyaml/<NAME_OF_NODE>.yaml</code> and
|
<code class="inlinecode"><HASS_CONFIG_FOLDER>/esphomeyaml/<NAME_OF_NODE>.yaml</code> and
|
||||||
you will be able to edit this file with the
|
you will be able to edit this file with the
|
||||||
|
@ -421,7 +363,7 @@
|
||||||
</a>.
|
</a>.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
See the <a href="https://esphomelib.com/esphomeyaml/index.html" target="_blank">esphomeyaml index</a>
|
See the <a href="https://esphomelib.com/esphomeyaml/index.html" target="_blank">ESPHome index</a>
|
||||||
for a list of supported sensors/devices.
|
for a list of supported sensors/devices.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -429,8 +371,8 @@
|
||||||
have time, I would be happy to help with issues and discuss new features.
|
have time, I would be happy to help with issues and discuss new features.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Star <a href="https://github.com/OttoWinter/esphomelib" target="_blank">esphomelib</a> and
|
Star <a href="https://github.com/OttoWinter/esphomelib" target="_blank">ESPHome Core</a> and
|
||||||
<a href="https://github.com/OttoWinter/esphomeyaml" target="_blank">esphomeyaml</a> on GitHub
|
<a href="https://github.com/OttoWinter/esphomeyaml" target="_blank">ESPHome</a> on GitHub
|
||||||
if you find this software awesome and report issues using the bug trackers there.
|
if you find this software awesome and report issues using the bug trackers there.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -508,7 +450,7 @@
|
||||||
<div class="tap-target-content">
|
<div class="tap-target-content">
|
||||||
<h5>Set up your first Node</h5>
|
<h5>Set up your first Node</h5>
|
||||||
<p>
|
<p>
|
||||||
Huh... It seems like you you don't have any esphomeyaml configuration files yet...
|
Huh... It seems like you you don't have any ESPHome configuration files yet...
|
||||||
Fortunately, there's a setup wizard that will step you through setting up your first node 🎉
|
Fortunately, there's a setup wizard that will step you through setting up your first node 🎉
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -522,7 +464,7 @@
|
||||||
<div class="footer-copyright">
|
<div class="footer-copyright">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
© 2018 Copyright Otto Winter, Made with <a class="grey-text text-lighten-4" href="https://materializecss.com/" target="_blank">Materialize</a>
|
© 2018 Copyright Otto Winter, Made with <a class="grey-text text-lighten-4" href="https://materializecss.com/" target="_blank">Materialize</a>
|
||||||
<a class="grey-text text-lighten-4 right" href="{{ docs_link }}" target="_blank">esphomeyaml {{ version }} Documentation</a>
|
<a class="grey-text text-lighten-4 right" href="{{ docs_link }}" target="_blank">ESPHome {{ version }} Documentation</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -3,8 +3,10 @@ import logging
|
||||||
import random
|
import random
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
from esphomeyaml.core import EsphomeyamlError
|
from esphomeyaml.core import EsphomeyamlError
|
||||||
|
from esphomeyaml.helpers import resolve_ip_address, is_ip_address
|
||||||
|
|
||||||
RESPONSE_OK = 0
|
RESPONSE_OK = 0
|
||||||
RESPONSE_REQUEST_AUTH = 1
|
RESPONSE_REQUEST_AUTH = 1
|
||||||
|
@ -221,50 +223,26 @@ def perform_ota(sock, password, file_handle, filename):
|
||||||
|
|
||||||
_LOGGER.info("OTA successful")
|
_LOGGER.info("OTA successful")
|
||||||
|
|
||||||
|
# Do not connect logs until it is fully on
|
||||||
def is_ip_address(host):
|
time.sleep(2)
|
||||||
parts = host.split('.')
|
|
||||||
if len(parts) != 4:
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
for p in parts:
|
|
||||||
int(p)
|
|
||||||
return True
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_ip_address(host):
|
|
||||||
if is_ip_address(host):
|
|
||||||
_LOGGER.info("Connecting to %s", host)
|
|
||||||
return host
|
|
||||||
|
|
||||||
_LOGGER.info("Resolving IP Address of %s", host)
|
|
||||||
hosts = [host]
|
|
||||||
if host.endswith('.local'):
|
|
||||||
hosts.append(host[:-6])
|
|
||||||
|
|
||||||
errors = []
|
|
||||||
for x in hosts:
|
|
||||||
try:
|
|
||||||
ip = socket.gethostbyname(x)
|
|
||||||
break
|
|
||||||
except socket.error as err:
|
|
||||||
errors.append(err)
|
|
||||||
else:
|
|
||||||
_LOGGER.error("Error resolving IP address of %s. Is it connected to WiFi?",
|
|
||||||
host)
|
|
||||||
|
|
||||||
_LOGGER.error("(If this error persists, please set a static IP address: "
|
|
||||||
"https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips)")
|
|
||||||
raise OTAError("Errors: {}".format(', '.join(str(x) for x in errors)))
|
|
||||||
|
|
||||||
_LOGGER.info(" -> %s", ip)
|
|
||||||
return ip
|
|
||||||
|
|
||||||
|
|
||||||
def run_ota_impl_(remote_host, remote_port, password, filename):
|
def run_ota_impl_(remote_host, remote_port, password, filename):
|
||||||
ip = resolve_ip_address(remote_host)
|
if is_ip_address(remote_host):
|
||||||
|
_LOGGER.info("Connecting to %s", remote_host)
|
||||||
|
ip = remote_host
|
||||||
|
else:
|
||||||
|
_LOGGER.info("Resolving IP address of %s", remote_host)
|
||||||
|
try:
|
||||||
|
ip = resolve_ip_address(remote_host)
|
||||||
|
except EsphomeyamlError as err:
|
||||||
|
_LOGGER.error("Error resolving IP address of %s. Is it connected to WiFi?",
|
||||||
|
remote_host)
|
||||||
|
_LOGGER.error("(If this error persists, please set a static IP address: "
|
||||||
|
"https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips)")
|
||||||
|
raise OTAError(err)
|
||||||
|
_LOGGER.info(" -> %s", ip)
|
||||||
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
sock.settimeout(10.0)
|
sock.settimeout(10.0)
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import print_function
|
||||||
import errno
|
import errno
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -75,3 +76,26 @@ def mkdir_p(path):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def is_ip_address(host):
|
||||||
|
parts = host.split('.')
|
||||||
|
if len(parts) != 4:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
for p in parts:
|
||||||
|
int(p)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_ip_address(host):
|
||||||
|
try:
|
||||||
|
ip = socket.gethostbyname(host)
|
||||||
|
except socket.error as err:
|
||||||
|
from esphomeyaml.core import EsphomeyamlError
|
||||||
|
|
||||||
|
raise EsphomeyamlError("Error resolving IP address: {}".format(err))
|
||||||
|
|
||||||
|
return ip
|
||||||
|
|
|
@ -27,6 +27,14 @@ class ServiceRegistry(dict):
|
||||||
|
|
||||||
|
|
||||||
def safe_print(message=""):
|
def safe_print(message=""):
|
||||||
|
from esphomeyaml.core import CORE
|
||||||
|
|
||||||
|
if CORE.dashboard:
|
||||||
|
try:
|
||||||
|
message = message.replace('\033', '\\033')
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print(message)
|
print(message)
|
||||||
return
|
return
|
||||||
|
@ -48,6 +56,29 @@ def shlex_quote(s):
|
||||||
return u"'" + s.replace(u"'", u"'\"'\"'") + u"'"
|
return u"'" + s.replace(u"'", u"'\"'\"'") + u"'"
|
||||||
|
|
||||||
|
|
||||||
|
class RedirectText(object):
|
||||||
|
def __init__(self, out):
|
||||||
|
self._out = out
|
||||||
|
|
||||||
|
def __getattr__(self, item):
|
||||||
|
return getattr(self._out, item)
|
||||||
|
|
||||||
|
def write(self, s):
|
||||||
|
from esphomeyaml.core import CORE
|
||||||
|
|
||||||
|
if CORE.dashboard:
|
||||||
|
try:
|
||||||
|
s = s.replace('\033', '\\033')
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._out.write(s)
|
||||||
|
|
||||||
|
# pylint: disable=no-self-use
|
||||||
|
def isatty(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def run_external_command(func, *cmd, **kwargs):
|
def run_external_command(func, *cmd, **kwargs):
|
||||||
def mock_exit(return_code):
|
def mock_exit(return_code):
|
||||||
raise SystemExit(return_code)
|
raise SystemExit(return_code)
|
||||||
|
@ -57,6 +88,9 @@ def run_external_command(func, *cmd, **kwargs):
|
||||||
full_cmd = u' '.join(shlex_quote(x) for x in cmd)
|
full_cmd = u' '.join(shlex_quote(x) for x in cmd)
|
||||||
_LOGGER.info(u"Running: %s", full_cmd)
|
_LOGGER.info(u"Running: %s", full_cmd)
|
||||||
|
|
||||||
|
sys.stdout = RedirectText(sys.stdout)
|
||||||
|
sys.stderr = RedirectText(sys.stderr)
|
||||||
|
|
||||||
capture_stdout = kwargs.get('capture_stdout', False)
|
capture_stdout = kwargs.get('capture_stdout', False)
|
||||||
if capture_stdout:
|
if capture_stdout:
|
||||||
sys.stdout = io.BytesIO()
|
sys.stdout = io.BytesIO()
|
||||||
|
@ -76,6 +110,11 @@ def run_external_command(func, *cmd, **kwargs):
|
||||||
sys.argv = orig_argv
|
sys.argv = orig_argv
|
||||||
sys.exit = orig_exit
|
sys.exit = orig_exit
|
||||||
|
|
||||||
|
if isinstance(sys.stdout, RedirectText):
|
||||||
|
sys.stdout = sys.__stdout__
|
||||||
|
if isinstance(sys.stderr, RedirectText):
|
||||||
|
sys.stderr = sys.__stderr__
|
||||||
|
|
||||||
if capture_stdout:
|
if capture_stdout:
|
||||||
# pylint: disable=lost-exception
|
# pylint: disable=lost-exception
|
||||||
stdout = sys.stdout.getvalue()
|
stdout = sys.stdout.getvalue()
|
||||||
|
|
|
@ -35,13 +35,6 @@ WIFI_BIG = """ __ ___ ______ _
|
||||||
\ /\ / | | | | |
|
\ /\ / | | | | |
|
||||||
\/ \/ |_|_| |_|
|
\/ \/ |_|_| |_|
|
||||||
"""
|
"""
|
||||||
MQTT_BIG = """ __ __ ____ _______ _______
|
|
||||||
| \/ |/ __ \__ __|__ __|
|
|
||||||
| \ / | | | | | | | |
|
|
||||||
| |\/| | | | | | | | |
|
|
||||||
| | | | |__| | | | | |
|
|
||||||
|_| |_|\___\_\ |_| |_|
|
|
||||||
"""
|
|
||||||
OTA_BIG = """ ____ _______
|
OTA_BIG = """ ____ _______
|
||||||
/ __ \__ __|/\\
|
/ __ \__ __|/\\
|
||||||
| | | | | | / \\
|
| | | | | | / \\
|
||||||
|
@ -50,7 +43,6 @@ OTA_BIG = """ ____ _______
|
||||||
\____/ |_/_/ \_\\
|
\____/ |_/_/ \_\\
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO handle escaping
|
|
||||||
BASE_CONFIG = u"""esphomeyaml:
|
BASE_CONFIG = u"""esphomeyaml:
|
||||||
name: {name}
|
name: {name}
|
||||||
platform: {platform}
|
platform: {platform}
|
||||||
|
@ -60,24 +52,21 @@ wifi:
|
||||||
ssid: '{ssid}'
|
ssid: '{ssid}'
|
||||||
password: '{psk}'
|
password: '{psk}'
|
||||||
|
|
||||||
mqtt:
|
|
||||||
broker: '{broker}'
|
|
||||||
username: '{mqtt_username}'
|
|
||||||
password: '{mqtt_password}'
|
|
||||||
|
|
||||||
# Enable logging
|
# Enable logging
|
||||||
logger:
|
logger:
|
||||||
|
|
||||||
|
# Enable Home Assistant API
|
||||||
|
api:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def wizard_file(**kwargs):
|
def wizard_file(**kwargs):
|
||||||
config = BASE_CONFIG.format(**kwargs)
|
config = BASE_CONFIG.format(**kwargs)
|
||||||
|
|
||||||
if kwargs['ota_password']:
|
if kwargs['password']:
|
||||||
config += u"ota:\n password: '{}'\n".format(kwargs['ota_password'])
|
config += u" password: '{0}'\n\nota:\n password: '{0}'\n".format(kwargs['password'])
|
||||||
else:
|
else:
|
||||||
config += u"ota:\n"
|
config += u"\nota:\n"
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
@ -135,11 +124,11 @@ def wizard(path):
|
||||||
return 1
|
return 1
|
||||||
safe_print("Hi there!")
|
safe_print("Hi there!")
|
||||||
sleep(1.5)
|
sleep(1.5)
|
||||||
safe_print("I'm the wizard of esphomeyaml :)")
|
safe_print("I'm the wizard of ESPHome :)")
|
||||||
sleep(1.25)
|
sleep(1.25)
|
||||||
safe_print("And I'm here to help you get started with esphomeyaml.")
|
safe_print("And I'm here to help you get started with ESPHome.")
|
||||||
sleep(2.0)
|
sleep(2.0)
|
||||||
safe_print("In 5 steps I'm going to guide you through creating a basic "
|
safe_print("In 4 steps I'm going to guide you through creating a basic "
|
||||||
"configuration file for your custom ESP8266/ESP32 firmware. Yay!")
|
"configuration file for your custom ESP8266/ESP32 firmware. Yay!")
|
||||||
sleep(3.0)
|
sleep(3.0)
|
||||||
safe_print()
|
safe_print()
|
||||||
|
@ -205,6 +194,8 @@ def wizard(path):
|
||||||
else:
|
else:
|
||||||
safe_print("For example \"{}\".".format(color("bold_white", 'nodemcuv2')))
|
safe_print("For example \"{}\".".format(color("bold_white", 'nodemcuv2')))
|
||||||
boards = list(ESP8266_BOARD_PINS.keys())
|
boards = list(ESP8266_BOARD_PINS.keys())
|
||||||
|
safe_print("Options: {}".format(', '.join(boards)))
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
board = raw_input(color("bold_white", "(board): "))
|
board = raw_input(color("bold_white", "(board): "))
|
||||||
try:
|
try:
|
||||||
|
@ -214,7 +205,6 @@ def wizard(path):
|
||||||
safe_print(color('red', "Sorry, I don't think the board \"{}\" exists."))
|
safe_print(color('red', "Sorry, I don't think the board \"{}\" exists."))
|
||||||
safe_print()
|
safe_print()
|
||||||
sleep(0.25)
|
sleep(0.25)
|
||||||
safe_print("Possible options are {}".format(', '.join(boards)))
|
|
||||||
safe_print()
|
safe_print()
|
||||||
|
|
||||||
safe_print(u"Way to go! You've chosen {} as your board.".format(color('cyan', board)))
|
safe_print(u"Way to go! You've chosen {} as your board.".format(color('cyan', board)))
|
||||||
|
@ -255,60 +245,26 @@ def wizard(path):
|
||||||
safe_print("Perfect! WiFi is now set up (you can create static IPs and so on later).")
|
safe_print("Perfect! WiFi is now set up (you can create static IPs and so on later).")
|
||||||
sleep(1.5)
|
sleep(1.5)
|
||||||
|
|
||||||
safe_print_step(4, MQTT_BIG)
|
safe_print_step(4, OTA_BIG)
|
||||||
safe_print("Almost there! Now let's setup MQTT so that your node can connect to the "
|
safe_print("Almost there! ESPHome can automatically upload custom firmwares over WiFi "
|
||||||
"outside world.")
|
"(over the air) and integrates into Home Assistant with a native API.")
|
||||||
safe_print()
|
|
||||||
sleep(1)
|
|
||||||
safe_print("Please enter the " + color('green', 'address') + " of your MQTT broker.")
|
|
||||||
safe_print()
|
|
||||||
safe_print("For example \"{}\".".format(color('bold_white', '192.168.178.84')))
|
|
||||||
broker = raw_input(color('bold_white', "(broker): "))
|
|
||||||
|
|
||||||
safe_print("Thanks! Now enter the " + color('green', 'username') + " and " +
|
|
||||||
color('green', 'password') +
|
|
||||||
" for the MQTT broker. Leave empty for no authentication.")
|
|
||||||
mqtt_username = raw_input(color('bold_white', '(username): '))
|
|
||||||
mqtt_password = ''
|
|
||||||
if mqtt_username:
|
|
||||||
mqtt_password = raw_input(color('bold_white', '(password): '))
|
|
||||||
|
|
||||||
show = '*' * len(mqtt_password)
|
|
||||||
if len(mqtt_password) >= 2:
|
|
||||||
show = mqtt_password[:2] + '*' * len(mqtt_password)
|
|
||||||
safe_print(u"MQTT Username: \"{}\"; Password: \"{}\""
|
|
||||||
u"".format(color('cyan', mqtt_username), color('cyan', show)))
|
|
||||||
else:
|
|
||||||
safe_print("No authentication for MQTT")
|
|
||||||
|
|
||||||
safe_print_step(5, OTA_BIG)
|
|
||||||
safe_print("Last step! esphomeyaml can automatically upload custom firmwares over WiFi "
|
|
||||||
"(over the air).")
|
|
||||||
safe_print("This can be insecure if you do not trust the WiFi network. Do you want to set "
|
safe_print("This can be insecure if you do not trust the WiFi network. Do you want to set "
|
||||||
"an " + color('green', 'OTA password') + " for remote updates?")
|
"a " + color('green', 'password') + " for connecting to this ESP?")
|
||||||
safe_print()
|
safe_print()
|
||||||
sleep(0.25)
|
sleep(0.25)
|
||||||
safe_print("Press ENTER for no password")
|
safe_print("Press ENTER for no password")
|
||||||
ota_password = raw_input(color('bold_white', '(password): '))
|
password = raw_input(color('bold_white', '(password): '))
|
||||||
|
|
||||||
wizard_write(path=path, name=name, platform=platform, board=board,
|
wizard_write(path=path, name=name, platform=platform, board=board,
|
||||||
ssid=ssid, psk=psk, broker=broker,
|
ssid=ssid, psk=psk, password=password)
|
||||||
mqtt_username=mqtt_username, mqtt_password=mqtt_password,
|
|
||||||
ota_password=ota_password)
|
|
||||||
|
|
||||||
safe_print()
|
safe_print()
|
||||||
safe_print(color('cyan', "DONE! I've now written a new configuration file to ") +
|
safe_print(color('cyan', "DONE! I've now written a new configuration file to ") +
|
||||||
color('bold_cyan', path))
|
color('bold_cyan', path))
|
||||||
safe_print()
|
safe_print()
|
||||||
safe_print("Next steps:")
|
safe_print("Next steps:")
|
||||||
safe_print(" > If you haven't already, enable MQTT discovery in Home Assistant:")
|
safe_print(" > Check your Home Assistant \"integrations\" screen. If all goes well, you "
|
||||||
safe_print()
|
"should see your ESP being discovered automatically.")
|
||||||
safe_print(color('bold_white', "# In your configuration.yaml"))
|
|
||||||
safe_print(color('bold_white', "mqtt:"))
|
|
||||||
safe_print(color('bold_white', u" broker: {}".format(broker)))
|
|
||||||
safe_print(color('bold_white', " # ..."))
|
|
||||||
safe_print(color('bold_white', " discovery: True"))
|
|
||||||
safe_print()
|
|
||||||
safe_print(" > Then follow the rest of the getting started guide:")
|
safe_print(" > Then follow the rest of the getting started guide:")
|
||||||
safe_print(" > https://esphomelib.com/esphomeyaml/guides/getting_started_command_line.html")
|
safe_print(" > https://esphomelib.com/esphomeyaml/guides/getting_started_command_line.html")
|
||||||
return 0
|
return 0
|
||||||
|
|
|
@ -279,6 +279,7 @@ def gather_lib_deps():
|
||||||
# Manual fix for AsyncTCP
|
# Manual fix for AsyncTCP
|
||||||
if CORE.config[CONF_ESPHOMEYAML].get(CONF_ARDUINO_VERSION) == ARDUINO_VERSION_ESP32_DEV:
|
if CORE.config[CONF_ESPHOMEYAML].get(CONF_ARDUINO_VERSION) == ARDUINO_VERSION_ESP32_DEV:
|
||||||
lib_deps.add('https://github.com/me-no-dev/AsyncTCP.git#idf-update')
|
lib_deps.add('https://github.com/me-no-dev/AsyncTCP.git#idf-update')
|
||||||
|
lib_deps.remove('AsyncTCP@1.0.1')
|
||||||
# avoid changing build flags order
|
# avoid changing build flags order
|
||||||
return sorted(x for x in lib_deps if x)
|
return sorted(x for x in lib_deps if x)
|
||||||
|
|
||||||
|
@ -376,6 +377,7 @@ def write_platformio_project():
|
||||||
f.write("app1, app, ota_1, 0x200000, 0x190000,\n")
|
f.write("app1, app, ota_1, 0x200000, 0x190000,\n")
|
||||||
f.write("eeprom, data, 0x99, 0x390000, 0x001000,\n")
|
f.write("eeprom, data, 0x99, 0x390000, 0x001000,\n")
|
||||||
f.write("spiffs, data, spiffs, 0x391000, 0x00F000\n")
|
f.write("spiffs, data, spiffs, 0x391000, 0x00F000\n")
|
||||||
|
write_gitignore()
|
||||||
write_platformio_ini(content, platformio_ini)
|
write_platformio_ini(content, platformio_ini)
|
||||||
|
|
||||||
|
|
||||||
|
@ -427,3 +429,23 @@ def clean_build():
|
||||||
continue
|
continue
|
||||||
_LOGGER.info("Deleting %s", dir_path)
|
_LOGGER.info("Deleting %s", dir_path)
|
||||||
shutil.rmtree(dir_path)
|
shutil.rmtree(dir_path)
|
||||||
|
|
||||||
|
|
||||||
|
GITIGNORE_CONTENT = """# Gitignore settings for esphomeyaml
|
||||||
|
# This is an example and may include too much for your use-case.
|
||||||
|
# You can modify this file to suit your needs.
|
||||||
|
/.esphomeyaml/
|
||||||
|
**/.pioenvs/
|
||||||
|
**/.piolibdeps/
|
||||||
|
**/lib/
|
||||||
|
**/src/
|
||||||
|
**/platformio.ini
|
||||||
|
/secrets.yaml
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def write_gitignore():
|
||||||
|
path = CORE.relative_path('.gitignore')
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
f.write(GITIGNORE_CONTENT)
|
||||||
|
|
1
pylintrc
1
pylintrc
|
@ -1,5 +1,6 @@
|
||||||
[MASTER]
|
[MASTER]
|
||||||
reports=no
|
reports=no
|
||||||
|
ignore=api_pb2.py
|
||||||
|
|
||||||
disable=
|
disable=
|
||||||
missing-docstring,
|
missing-docstring,
|
||||||
|
|
|
@ -6,3 +6,4 @@ colorlog>=3.1.2
|
||||||
tornado>=5.0.0
|
tornado>=5.0.0
|
||||||
esptool>=2.3.1
|
esptool>=2.3.1
|
||||||
typing>=3.0.0
|
typing>=3.0.0
|
||||||
|
protobuf>=3.4
|
||||||
|
|
|
@ -4,3 +4,4 @@ description-file = README.md
|
||||||
[flake8]
|
[flake8]
|
||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
builtins = unicode, long, raw_input
|
builtins = unicode, long, raw_input
|
||||||
|
exclude = api_pb2.py
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -30,6 +30,7 @@ REQUIRES = [
|
||||||
'tornado>=5.0.0',
|
'tornado>=5.0.0',
|
||||||
'esptool>=2.3.1',
|
'esptool>=2.3.1',
|
||||||
'typing>=3.0.0',
|
'typing>=3.0.0',
|
||||||
|
'protobuf>=3.4',
|
||||||
]
|
]
|
||||||
|
|
||||||
CLASSIFIERS = [
|
CLASSIFIERS = [
|
||||||
|
|
10
tests/.gitignore
vendored
Normal file
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