HassIO -> dashboard

This commit is contained in:
Otto Winter 2018-05-27 14:15:24 +02:00
parent 2e7d8540fb
commit 93d962dd43
No known key found for this signature in database
GPG key ID: DB66C0BE6013F97E
15 changed files with 79 additions and 57 deletions

View file

@ -1,4 +1,4 @@
include README.md include README.md
include esphomeyaml/hassio/templates/index.html include esphomeyaml/dashboard/templates/index.html
include esphomeyaml/hassio/static/materialize-stepper.min.css include esphomeyaml/dashboard/static/materialize-stepper.min.css
include esphomeyaml/hassio/static/materialize-stepper.min.js include esphomeyaml/dashboard/static/materialize-stepper.min.js

View file

@ -314,16 +314,16 @@ def command_version(args):
return 0 return 0
def command_hassio(args): def command_dashboard(args):
from esphomeyaml.hassio import hassio from esphomeyaml.dashboard import dashboard
return hassio.start_web_server(args) return dashboard.start_web_server(args)
PRE_CONFIG_ACTIONS = { PRE_CONFIG_ACTIONS = {
'wizard': command_wizard, 'wizard': command_wizard,
'version': command_version, 'version': command_version,
'hassio': command_hassio 'dashboard': command_dashboard
} }
POST_CONFIG_ACTIONS = { POST_CONFIG_ACTIONS = {
@ -353,7 +353,7 @@ def parse_args(argv):
"For example /dev/cu.SLAB_USBtoUART.") "For example /dev/cu.SLAB_USBtoUART.")
parser_upload.add_argument('--host-port', help="Specify the host port.", type=int) parser_upload.add_argument('--host-port', help="Specify the host port.", type=int)
parser_upload.add_argument('--use-esptoolpy', parser_upload.add_argument('--use-esptoolpy',
help="Use esptool.py for HassIO (only for ESP8266)", help="Use esptool.py for the uploading (only for ESP8266)",
action='store_true') action='store_true')
parser_logs = subparsers.add_parser('logs', help='Validate the configuration ' parser_logs = subparsers.add_parser('logs', help='Validate the configuration '
@ -364,7 +364,7 @@ def parse_args(argv):
parser_logs.add_argument('--client-id', help='Manually set the client id.') parser_logs.add_argument('--client-id', help='Manually set the client id.')
parser_logs.add_argument('--serial-port', help="Manually specify a serial port to use" parser_logs.add_argument('--serial-port', help="Manually specify a serial port to use"
"For example /dev/cu.SLAB_USBtoUART.") "For example /dev/cu.SLAB_USBtoUART.")
parser_logs.add_argument('--escape', help="Escape ANSI color codes for HassIO", parser_logs.add_argument('--escape', help="Escape ANSI color codes for running in dashboard",
action='store_true') action='store_true')
parser_run = subparsers.add_parser('run', help='Validate the configuration, create a binary, ' parser_run = subparsers.add_parser('run', help='Validate the configuration, create a binary, '
@ -378,9 +378,10 @@ def parse_args(argv):
parser_run.add_argument('--username', help='Manually set the MQTT username for logs.') parser_run.add_argument('--username', help='Manually set the MQTT username for logs.')
parser_run.add_argument('--password', help='Manually set the MQTT password for logs.') parser_run.add_argument('--password', help='Manually set the MQTT password for logs.')
parser_run.add_argument('--client-id', help='Manually set the client id for logs.') parser_run.add_argument('--client-id', help='Manually set the client id for logs.')
parser_run.add_argument('--escape', help="Escape ANSI color codes for HassIO", parser_run.add_argument('--escape', help="Escape ANSI color codes for running in dashboard",
action='store_true') action='store_true')
parser_run.add_argument('--use-esptoolpy', help="Use esptool.py for HassIO (only for ESP8266)", parser_run.add_argument('--use-esptoolpy',
help="Use esptool.py for the uploading (only for ESP8266)",
action='store_true') action='store_true')
parser_clean = subparsers.add_parser('clean-mqtt', help="Helper to clear an MQTT topic from " parser_clean = subparsers.add_parser('clean-mqtt', help="Helper to clear an MQTT topic from "
@ -397,8 +398,9 @@ def parse_args(argv):
subparsers.add_parser('version', help="Print the esphomeyaml version and exit.") subparsers.add_parser('version', help="Print the esphomeyaml version and exit.")
hassio = subparsers.add_parser('hassio', help="Create a simple webserver for a HassIO add-on.") dashboard = subparsers.add_parser('dashboard',
hassio.add_argument("--port", help="The HTTP port to open connections on.", type=int, help="Create a simple webserver for a dashboard.")
dashboard.add_argument("--port", help="The HTTP port to open connections on.", type=int,
default=6052) default=6052)
return parser.parse_args(argv[1:]) return parser.parse_args(argv[1:])

View file

@ -4,9 +4,9 @@ import esphomeyaml.config_validation as cv
from esphomeyaml.components import cover, fan from esphomeyaml.components import cover, fan
from esphomeyaml.const import CONF_ACTION_ID, CONF_AND, CONF_AUTOMATION_ID, CONF_BLUE, \ from esphomeyaml.const import CONF_ACTION_ID, CONF_AND, CONF_AUTOMATION_ID, CONF_BLUE, \
CONF_BRIGHTNESS, CONF_CONDITION_ID, CONF_DELAY, CONF_EFFECT, CONF_FLASH_LENGTH, CONF_GREEN, \ CONF_BRIGHTNESS, CONF_CONDITION_ID, CONF_DELAY, CONF_EFFECT, CONF_FLASH_LENGTH, CONF_GREEN, \
CONF_ID, CONF_IF, CONF_LAMBDA, CONF_MAX, CONF_MIN, CONF_OR, CONF_PAYLOAD, CONF_QOS, \ CONF_ID, CONF_IF, CONF_LAMBDA, CONF_OR, CONF_OSCILLATING, CONF_PAYLOAD, \
CONF_RANGE, CONF_RED, CONF_RETAIN, CONF_THEN, CONF_TOPIC, CONF_TRANSITION_LENGTH, \ CONF_QOS, CONF_RANGE, CONF_RED, CONF_RETAIN, CONF_SPEED, CONF_THEN, CONF_TOPIC, \
CONF_TRIGGER_ID, CONF_WHITE, CONF_OSCILLATING, CONF_SPEED CONF_TRANSITION_LENGTH, CONF_TRIGGER_ID, CONF_WHITE, CONF_ABOVE, CONF_BELOW
from esphomeyaml.core import ESPHomeYAMLError from esphomeyaml.core import ESPHomeYAMLError
from esphomeyaml.helpers import App, ArrayInitializer, Pvariable, TemplateArguments, add, \ from esphomeyaml.helpers import App, ArrayInitializer, Pvariable, TemplateArguments, add, \
bool_, esphomelib_ns, float_, get_variable, process_lambda, std_string, templatable, uint32, \ bool_, esphomelib_ns, float_, get_variable, process_lambda, std_string, templatable, uint32, \
@ -51,11 +51,11 @@ ACTIONS_SCHEMA = vol.All(cv.ensure_list, [vol.All({
vol.Required(CONF_ID): cv.variable_id, vol.Required(CONF_ID): cv.variable_id,
vol.Optional(CONF_TRANSITION_LENGTH): cv.templatable(cv.positive_time_period_milliseconds), vol.Optional(CONF_TRANSITION_LENGTH): cv.templatable(cv.positive_time_period_milliseconds),
vol.Optional(CONF_FLASH_LENGTH): cv.templatable(cv.positive_time_period_milliseconds), vol.Optional(CONF_FLASH_LENGTH): cv.templatable(cv.positive_time_period_milliseconds),
vol.Optional(CONF_BRIGHTNESS): cv.templatable(cv.zero_to_one_float), vol.Optional(CONF_BRIGHTNESS): cv.templatable(cv.percentage),
vol.Optional(CONF_RED): cv.templatable(cv.zero_to_one_float), vol.Optional(CONF_RED): cv.templatable(cv.percentage),
vol.Optional(CONF_GREEN): cv.templatable(cv.zero_to_one_float), vol.Optional(CONF_GREEN): cv.templatable(cv.percentage),
vol.Optional(CONF_BLUE): cv.templatable(cv.zero_to_one_float), vol.Optional(CONF_BLUE): cv.templatable(cv.percentage),
vol.Optional(CONF_WHITE): cv.templatable(cv.zero_to_one_float), vol.Optional(CONF_WHITE): cv.templatable(cv.percentage),
vol.Optional(CONF_EFFECT): cv.templatable(cv.string), vol.Optional(CONF_EFFECT): cv.templatable(cv.string),
}), }),
vol.Optional(CONF_SWITCH_TOGGLE): vol.Schema({ vol.Optional(CONF_SWITCH_TOGGLE): vol.Schema({
@ -116,9 +116,9 @@ CONDITIONS_SCHEMA = vol.All(cv.ensure_list, [vol.All({
vol.Optional(CONF_AND): validate_recursive_condition, vol.Optional(CONF_AND): validate_recursive_condition,
vol.Optional(CONF_OR): validate_recursive_condition, vol.Optional(CONF_OR): validate_recursive_condition,
vol.Optional(CONF_RANGE): vol.All(vol.Schema({ vol.Optional(CONF_RANGE): vol.All(vol.Schema({
vol.Optional(CONF_MIN): vol.Coerce(float), vol.Optional(CONF_ABOVE): vol.Coerce(float),
vol.Optional(CONF_MAX): vol.Coerce(float), vol.Optional(CONF_BELOW): vol.Coerce(float),
}), cv.has_at_least_one_key(CONF_MIN, CONF_MAX)), }), cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW)),
vol.Optional(CONF_LAMBDA): cv.lambda_, vol.Optional(CONF_LAMBDA): cv.lambda_,
}), cv.has_at_exactly_one_key(*CONDITION_KEYS)]) }), cv.has_at_exactly_one_key(*CONDITION_KEYS)])
@ -149,10 +149,10 @@ def build_condition(config, arg_type):
conf = config[CONF_RANGE] conf = config[CONF_RANGE]
rhs = RangeCondition.new(template_arg) rhs = RangeCondition.new(template_arg)
condition = Pvariable(RangeCondition.template(template_arg), config[CONF_CONDITION_ID], rhs) condition = Pvariable(RangeCondition.template(template_arg), config[CONF_CONDITION_ID], rhs)
if CONF_MIN in conf: if CONF_ABOVE in conf:
condition.set_min(templatable(conf[CONF_MIN], arg_type, float_)) condition.set_min(templatable(conf[CONF_ABOVE], arg_type, float_))
if CONF_MAX in conf: if CONF_BELOW in conf:
condition.set_max(templatable(conf[CONF_MAX], arg_type, float_)) condition.set_max(templatable(conf[CONF_BELOW], arg_type, float_))
return condition return condition
raise ESPHomeYAMLError(u"Unsupported condition {}".format(config)) raise ESPHomeYAMLError(u"Unsupported condition {}".format(config))

View file

@ -18,7 +18,7 @@ def validate_voltage(values):
value = cv.string(value) value = cv.string(value)
if not value.endswith('V'): if not value.endswith('V'):
value += 'V' value += 'V'
return cv.one_of(values)(value) return cv.one_of(*values)(value)
return validator return validator

View file

@ -14,9 +14,9 @@ PLATFORM_SCHEMA = fan.PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SPEED_COMMAND_TOPIC): cv.subscribe_topic, vol.Optional(CONF_SPEED_COMMAND_TOPIC): cv.subscribe_topic,
vol.Optional(CONF_OSCILLATION_OUTPUT): cv.variable_id, vol.Optional(CONF_OSCILLATION_OUTPUT): cv.variable_id,
vol.Optional(CONF_SPEED): vol.Schema({ vol.Optional(CONF_SPEED): vol.Schema({
vol.Required(CONF_LOW): cv.zero_to_one_float, vol.Required(CONF_LOW): cv.percentage,
vol.Required(CONF_MEDIUM): cv.zero_to_one_float, vol.Required(CONF_MEDIUM): cv.percentage,
vol.Required(CONF_HIGH): cv.zero_to_one_float, vol.Required(CONF_HIGH): cv.percentage,
}), }),
}).extend(fan.FAN_SCHEMA.schema) }).extend(fan.FAN_SCHEMA.schema)

View file

@ -14,7 +14,7 @@ BINARY_OUTPUT_SCHEMA = cv.REQUIRED_ID_SCHEMA.extend({
}) })
FLOAT_OUTPUT_SCHEMA = BINARY_OUTPUT_SCHEMA.extend({ FLOAT_OUTPUT_SCHEMA = BINARY_OUTPUT_SCHEMA.extend({
vol.Optional(CONF_MAX_POWER): cv.zero_to_one_float, vol.Optional(CONF_MAX_POWER): cv.percentage,
}) })
output_ns = esphomelib_ns.namespace('output') output_ns = esphomelib_ns.namespace('output')

View file

@ -2,9 +2,9 @@ import voluptuous as vol
import esphomeyaml.config_validation as cv import esphomeyaml.config_validation as cv
from esphomeyaml import automation from esphomeyaml import automation
from esphomeyaml.const import CONF_ACCURACY_DECIMALS, CONF_ALPHA, CONF_DEBOUNCE, CONF_DELTA, \ from esphomeyaml.const import CONF_ABOVE, CONF_ACCURACY_DECIMALS, CONF_ALPHA, CONF_BELOW, \
CONF_EXPIRE_AFTER, CONF_EXPONENTIAL_MOVING_AVERAGE, CONF_FILTERS, CONF_FILTER_NAN, \ CONF_DEBOUNCE, CONF_DELTA, CONF_EXPIRE_AFTER, CONF_EXPONENTIAL_MOVING_AVERAGE, CONF_FILTERS, \
CONF_FILTER_OUT, CONF_HEARTBEAT, CONF_ICON, CONF_ID, CONF_LAMBDA, CONF_MAX, CONF_MIN, \ CONF_FILTER_NAN, CONF_FILTER_OUT, CONF_HEARTBEAT, CONF_ICON, CONF_ID, CONF_LAMBDA, \
CONF_MQTT_ID, CONF_MULTIPLY, CONF_NAME, CONF_OFFSET, CONF_ON_RAW_VALUE, CONF_ON_VALUE,\ CONF_MQTT_ID, CONF_MULTIPLY, CONF_NAME, CONF_OFFSET, CONF_ON_RAW_VALUE, CONF_ON_VALUE,\
CONF_ON_VALUE_RANGE, CONF_OR, CONF_SEND_EVERY, CONF_SLIDING_WINDOW_MOVING_AVERAGE, \ CONF_ON_VALUE_RANGE, CONF_OR, CONF_SEND_EVERY, CONF_SLIDING_WINDOW_MOVING_AVERAGE, \
CONF_THROTTLE, CONF_TRIGGER_ID, CONF_UNIQUE, CONF_UNIT_OF_MEASUREMENT, CONF_WINDOW_SIZE CONF_THROTTLE, CONF_TRIGGER_ID, CONF_UNIQUE, CONF_UNIT_OF_MEASUREMENT, CONF_WINDOW_SIZE
@ -59,9 +59,9 @@ SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend({
vol.Optional(CONF_ON_RAW_VALUE): vol.All(cv.ensure_list, [automation.AUTOMATION_SCHEMA]), vol.Optional(CONF_ON_RAW_VALUE): vol.All(cv.ensure_list, [automation.AUTOMATION_SCHEMA]),
vol.Optional(CONF_ON_VALUE_RANGE): vol.All(cv.ensure_list, [vol.All( vol.Optional(CONF_ON_VALUE_RANGE): vol.All(cv.ensure_list, [vol.All(
automation.AUTOMATION_SCHEMA.extend({ automation.AUTOMATION_SCHEMA.extend({
vol.Optional(CONF_MIN): vol.Coerce(float), vol.Optional(CONF_ABOVE): vol.Coerce(float),
vol.Optional(CONF_MAX): vol.Coerce(float), vol.Optional(CONF_BELOW): vol.Coerce(float),
}), cv.has_at_least_one_key(CONF_MIN, CONF_MAX))]), }), cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW))]),
}) })
# pylint: disable=invalid-name # pylint: disable=invalid-name
@ -144,10 +144,10 @@ def setup_sensor_core_(sensor_var, mqtt_var, config):
for conf in config.get(CONF_ON_VALUE_RANGE, []): for conf in config.get(CONF_ON_VALUE_RANGE, []):
rhs = sensor_var.make_value_range_trigger() rhs = sensor_var.make_value_range_trigger()
trigger = Pvariable(ValueRangeTrigger, conf[CONF_TRIGGER_ID], rhs) trigger = Pvariable(ValueRangeTrigger, conf[CONF_TRIGGER_ID], rhs)
if CONF_MIN in conf: if CONF_ABOVE in conf:
trigger.set_min(templatable(conf[CONF_MIN], float_, float_)) trigger.set_min(templatable(conf[CONF_ABOVE], float_, float_))
if CONF_MAX in conf: if CONF_BELOW in conf:
trigger.set_max(templatable(conf[CONF_MAX], float_, float_)) trigger.set_max(templatable(conf[CONF_BELOW], float_, float_))
automation.build_automation(trigger, float_, conf) automation.build_automation(trigger, float_, conf)
if CONF_EXPIRE_AFTER in config: if CONF_EXPIRE_AFTER in config:

View file

@ -36,14 +36,18 @@ def validate_gain(value):
elif not isinstance(value, (str, unicode)): elif not isinstance(value, (str, unicode)):
raise vol.Invalid('invalid gain "{}"'.format(value)) raise vol.Invalid('invalid gain "{}"'.format(value))
if value not in GAIN: return cv.one_of(*GAIN)(value)
raise vol.Invalid("Invalid gain, options are {}".format(', '.join(GAIN.keys())))
return value
def validate_mux(value):
value = cv.string(value).upper()
value = value.replace(' ', '_')
return cv.one_of(*MUX)(value)
PLATFORM_SCHEMA = sensor.PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = sensor.PLATFORM_SCHEMA.extend({
cv.GenerateID('ads1115_sensor'): cv.register_variable_id, cv.GenerateID('ads1115_sensor'): cv.register_variable_id,
vol.Required(CONF_MULTIPLEXER): vol.All(vol.Upper, cv.one_of(*MUX)), vol.Required(CONF_MULTIPLEXER): validate_mux,
vol.Required(CONF_GAIN): validate_gain, vol.Required(CONF_GAIN): validate_gain,
vol.Optional(CONF_ADS1115_ID): cv.variable_id, vol.Optional(CONF_ADS1115_ID): cv.variable_id,
vol.Optional(CONF_UPDATE_INTERVAL): cv.positive_time_period_milliseconds, vol.Optional(CONF_UPDATE_INTERVAL): cv.positive_time_period_milliseconds,

View file

@ -461,6 +461,12 @@ hex_uint32_t = vol.All(hex_int, vol.Range(min=0, max=4294967295))
i2c_address = hex_uint8_t i2c_address = hex_uint8_t
def percentage(value):
if isinstance(value, (str, unicode)) and value.endswith('%'):
value = float(value[:-1].rstrip()) / 100.0
return zero_to_one_float(value)
def invalid(message): def invalid(message):
def validator(value): def validator(value):
raise vol.Invalid(message) raise vol.Invalid(message)

View file

@ -81,8 +81,8 @@ CONF_TRANSITION_LENGTH = 'transition_length'
CONF_FLASH_LENGTH = 'flash_length' CONF_FLASH_LENGTH = 'flash_length'
CONF_BRIGHTNESS = 'brightness' CONF_BRIGHTNESS = 'brightness'
CONF_EFFECT = 'effect' CONF_EFFECT = 'effect'
CONF_MIN = 'min' CONF_ABOVE = 'above'
CONF_MAX = 'max' CONF_BELOW = 'below'
CONF_ON = 'on' CONF_ON = 'on'
CONF_IF = 'if' CONF_IF = 'if'
CONF_THEN = 'then' CONF_THEN = 'then'

View file

@ -140,7 +140,7 @@ class DownloadBinaryRequestHandler(tornado.web.RequestHandler):
class MainRequestHandler(tornado.web.RequestHandler): class MainRequestHandler(tornado.web.RequestHandler):
def get(self): def get(self):
files = [f for f in os.listdir(CONFIG_DIR) if f.endswith('.yaml')] files = sorted([f for f in os.listdir(CONFIG_DIR) if f.endswith('.yaml')])
full_path_files = [os.path.join(CONFIG_DIR, f) for f in files] full_path_files = [os.path.join(CONFIG_DIR, f) for f in files]
self.render("templates/index.html", files=files, full_path_files=full_path_files, self.render("templates/index.html", files=files, full_path_files=full_path_files,
version=const.__version__) version=const.__version__)
@ -157,7 +157,7 @@ def make_app():
(r"/serial-ports", SerialPortRequestHandler), (r"/serial-ports", SerialPortRequestHandler),
(r"/wizard.html", WizardRequestHandler), (r"/wizard.html", WizardRequestHandler),
(r'/static/(.*)', tornado.web.StaticFileHandler, {'path': static_path}), (r'/static/(.*)', tornado.web.StaticFileHandler, {'path': static_path}),
], debug=True) ], debug=False)
def start_web_server(args): def start_web_server(args):
@ -166,7 +166,7 @@ def start_web_server(args):
if not os.path.exists(CONFIG_DIR): if not os.path.exists(CONFIG_DIR):
os.makedirs(CONFIG_DIR) os.makedirs(CONFIG_DIR)
_LOGGER.info("Starting HassIO add-on web server on port %s and configuration dir %s...", _LOGGER.info("Starting dashboard web server on port %s and configuration dir %s...",
args.port, CONFIG_DIR) args.port, CONFIG_DIR)
app = make_app() app = make_app()
app.listen(args.port) app.listen(args.port)

View file

@ -547,7 +547,11 @@
const msg = data.data; const msg = data.data;
log.innerHTML += colorReplace(msg); log.innerHTML += colorReplace(msg);
} else if (data.event === "exit") { } else if (data.event === "exit") {
M.toast({html: `Program exited with code ${data.code}`}); if (data.code === 0) {
M.toast({html: "Program exited successfully!"});
} else {
M.toast({html: `Program failed with code ${data.code}`});
}
} }
}); });
logSocket.addEventListener('open', () => { logSocket.addEventListener('open', () => {
@ -615,7 +619,11 @@
const msg = data.data; const msg = data.data;
log.innerHTML += colorReplace(msg); log.innerHTML += colorReplace(msg);
} else if (data.event === "exit") { } else if (data.event === "exit") {
M.toast({html: `Program exited with code ${data.code}`}); if (data.code === 0) {
M.toast({html: "Program exited successfully!"});
} else {
M.toast({html: `Program failed with code ${data.code}`});
}
} }
}); });
logSocket.addEventListener('open', () => { logSocket.addEventListener('open', () => {
@ -665,9 +673,11 @@
const msg = data.data; const msg = data.data;
log.innerHTML += colorReplace(msg); log.innerHTML += colorReplace(msg);
} else if (data.event === "exit") { } else if (data.event === "exit") {
M.toast({html: `Program exited with code ${data.code}`});
if (data.code === 0) { if (data.code === 0) {
M.toast({html: "Program exited successfully!"});
downloadButton.classList.remove('disabled'); downloadButton.classList.remove('disabled');
} else {
M.toast({html: `Program failed with code ${data.code}`});
} }
} }
}); });