Merge branch 'dev' into ttp229-bsf

This commit is contained in:
Otto Winter 2019-05-13 13:01:08 +02:00
commit 3e044db9f1
No known key found for this signature in database
GPG key ID: DB66C0BE6013F97E
106 changed files with 1606 additions and 612 deletions

13
.gitignore vendored
View file

@ -6,6 +6,19 @@ __pycache__/
# C extensions
*.so
# Hide sublime text stuff
*.sublime-project
*.sublime-workspace
# Hide some OS X stuff
.DS_Store
.AppleDouble
.LSOverride
Icon
# Thumbnails
._*
# Distribution / packaging
.Python
build/

View file

@ -51,6 +51,6 @@ matrix:
- clang-format-7 -version
- clang-apply-replacements-7 -version
script:
- script/clang-tidy.py --all-headers -j 2 --fix
- script/clang-format.py -i -j 2
- script/clang-tidy --all-headers -j 2 --fix
- script/clang-format -i -j 2
- script/ci-suggest-changes

View file

@ -1,16 +1,18 @@
from __future__ import print_function
import argparse
import functools
import logging
import os
import sys
from datetime import datetime
from esphome import const, writer, yaml_util
import esphome.codegen as cg
from esphome.config import iter_components, read_config, strip_default_ids
from esphome.const import CONF_BAUD_RATE, CONF_BROKER, CONF_LOGGER, CONF_OTA, \
CONF_PASSWORD, CONF_PORT
from esphome.core import CORE, EsphomeError
from esphome.core import CORE, EsphomeError, coroutine, coroutine_with_priority
from esphome.helpers import color, indent
from esphome.py_compat import IS_PY2, safe_input
from esphome.util import run_external_command, run_external_process, safe_print
@ -117,12 +119,27 @@ def run_miniterm(config, port):
config, line, backtrace_state=backtrace_state)
def wrap_to_code(name, comp):
coro = coroutine(comp.to_code)
@functools.wraps(comp.to_code)
@coroutine_with_priority(coro.priority)
def wrapped(conf):
cg.add(cg.LineComment(u"{}:".format(name)))
if comp.config_schema is not None:
cg.add(cg.LineComment(indent(yaml_util.dump(conf).decode('utf-8'))))
yield coro(conf)
return wrapped
def write_cpp(config):
_LOGGER.info("Generating C++ source...")
for _, component, conf in iter_components(CORE.config):
for name, component, conf in iter_components(CORE.config):
if component.to_code is not None:
CORE.add_job(component.to_code, conf)
coro = wrap_to_code(name, component)
CORE.add_job(coro, conf)
CORE.flush_tasks()
@ -245,7 +262,7 @@ def command_vscode(args):
from esphome import vscode
CORE.config_path = args.configuration
vscode.read_config()
vscode.read_config(args)
def command_compile(args, config):
@ -423,7 +440,8 @@ def parse_args(argv):
dashboard.add_argument("--socket",
help="Make the dashboard serve under a unix socket", type=str)
subparsers.add_parser('vscode', help=argparse.SUPPRESS)
vscode = subparsers.add_parser('vscode', help=argparse.SUPPRESS)
vscode.add_argument('--ace', action='store_true')
return parser.parse_args(argv[1:])

View file

@ -10,16 +10,16 @@
# pylint: disable=unused-import
from esphome.cpp_generator import ( # noqa
Expression, RawExpression, RawStatement, TemplateArguments,
StructInitializer, ArrayInitializer, safe_exp, Statement,
StructInitializer, ArrayInitializer, safe_exp, Statement, LineComment,
progmem_array, statement, variable, Pvariable, new_Pvariable,
add, add_global, add_library, add_build_flag, add_define,
get_variable, get_variable_with_full_id, process_lambda, is_template, templatable, MockObj,
MockObjClass)
from esphome.cpp_helpers import ( # noqa
gpio_pin_expression, register_component, build_registry_entry,
build_registry_list, extract_registry_entry_config)
build_registry_list, extract_registry_entry_config, register_parented)
from esphome.cpp_types import ( # noqa
global_ns, void, nullptr, float_, bool_, std_ns, std_string,
global_ns, void, nullptr, float_, double, bool_, std_ns, std_string,
std_vector, uint8, uint16, uint32, int32, const_char_ptr, NAN,
esphome_ns, App, Nameable, Component, ComponentPtr,
PollingComponent, Application, optional, arduino_json_ns, JsonObject,

View file

@ -87,7 +87,7 @@ HOMEASSISTANT_SERVICE_ACTION_SCHEMA = cv.Schema({
cv.string: cv.string,
}),
cv.Optional(CONF_VARIABLES): cv.Schema({
cv.string: cv.lambda_,
cv.string: cv.returning_lambda,
}),
})

View file

@ -70,7 +70,7 @@ def delayed_off_filter_to_code(config, filter_id):
yield var
@FILTER_REGISTRY.register('lambda', LambdaFilter, cv.lambda_)
@FILTER_REGISTRY.register('lambda', LambdaFilter, cv.returning_lambda)
def lambda_filter_to_code(config, filter_id):
lambda_ = yield cg.process_lambda(config, [(bool, 'x')], return_type=cg.optional.template(bool))
yield cg.new_Pvariable(filter_id, lambda_)

View file

@ -5,7 +5,7 @@ namespace esphome {
namespace binary_sensor {
static const char *TAG = "something.Filter";
static const char *TAG = "sensor.filter";
void Filter::output(bool value, bool is_initial) {
if (!this->dedup_.next(value))

View file

@ -6,7 +6,7 @@
namespace esphome {
namespace ble_presence {
static const char *TAG = "something.something";
static const char *TAG = "ble_presence";
void BLEPresenceDevice::dump_config() { LOG_BINARY_SENSOR("", "BLE Presence", this); }

View file

@ -207,11 +207,6 @@ ClimateTraits Climate::get_traits() {
return traits;
}
#ifdef USE_MQTT_CLIMATE
MQTTClimateComponent *Climate::get_mqtt() const { return this->mqtt_; }
void Climate::set_mqtt(MQTTClimateComponent *mqtt) { this->mqtt_ = mqtt; }
#endif
void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) {
this->visual_min_temperature_override_ = visual_min_temperature_override;
}

View file

@ -169,11 +169,6 @@ class Climate : public Nameable {
*/
ClimateTraits get_traits();
#ifdef USE_MQTT_COVER
MQTTClimateComponent *get_mqtt() const;
void set_mqtt(MQTTClimateComponent *mqtt);
#endif
void set_visual_min_temperature_override(float visual_min_temperature_override);
void set_visual_max_temperature_override(float visual_max_temperature_override);
void set_visual_temperature_step_override(float visual_temperature_step_override);

View file

@ -6,13 +6,13 @@ namespace climate {
const char *climate_mode_to_string(ClimateMode mode) {
switch (mode) {
case CLIMATE_MODE_OFF:
return "OFF";
return "off";
case CLIMATE_MODE_AUTO:
return "AUTO";
return "auto";
case CLIMATE_MODE_COOL:
return "COOL";
return "cool";
case CLIMATE_MODE_HEAT:
return "HEAT";
return "heat";
default:
return "UNKNOWN";
}

View file

@ -8,7 +8,7 @@ CustomBinarySensorConstructor = custom_ns.class_('CustomBinarySensorConstructor'
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(CustomBinarySensorConstructor),
cv.Required(CONF_LAMBDA): cv.lambda_,
cv.Required(CONF_LAMBDA): cv.returning_lambda,
cv.Required(CONF_BINARY_SENSORS): cv.ensure_list(binary_sensor.BINARY_SENSOR_SCHEMA),
})

View file

@ -9,7 +9,7 @@ CustomFloatOutputConstructor = custom_ns.class_('CustomFloatOutputConstructor')
BINARY_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(CustomBinaryOutputConstructor),
cv.Required(CONF_LAMBDA): cv.lambda_,
cv.Required(CONF_LAMBDA): cv.returning_lambda,
cv.Required(CONF_TYPE): 'binary',
cv.Required(CONF_OUTPUTS):
cv.ensure_list(output.BINARY_OUTPUT_SCHEMA.extend({
@ -19,7 +19,7 @@ BINARY_SCHEMA = cv.Schema({
FLOAT_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(CustomFloatOutputConstructor),
cv.Required(CONF_LAMBDA): cv.lambda_,
cv.Required(CONF_LAMBDA): cv.returning_lambda,
cv.Required(CONF_TYPE): 'float',
cv.Required(CONF_OUTPUTS):
cv.ensure_list(output.FLOAT_OUTPUT_SCHEMA.extend({

View file

@ -8,7 +8,7 @@ CustomSensorConstructor = custom_ns.class_('CustomSensorConstructor')
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(CustomSensorConstructor),
cv.Required(CONF_LAMBDA): cv.lambda_,
cv.Required(CONF_LAMBDA): cv.returning_lambda,
cv.Required(CONF_SENSORS): cv.ensure_list(sensor.SENSOR_SCHEMA),
})

View file

@ -8,7 +8,7 @@ CustomSwitchConstructor = custom_ns.class_('CustomSwitchConstructor')
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(CustomSwitchConstructor),
cv.Required(CONF_LAMBDA): cv.lambda_,
cv.Required(CONF_LAMBDA): cv.returning_lambda,
cv.Required(CONF_SWITCHES):
cv.ensure_list(switch.SWITCH_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(switch.Switch),

View file

@ -8,7 +8,7 @@ CustomTextSensorConstructor = custom_ns.class_('CustomTextSensorConstructor')
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(CustomTextSensorConstructor),
cv.Required(CONF_LAMBDA): cv.lambda_,
cv.Required(CONF_LAMBDA): cv.returning_lambda,
cv.Required(CONF_TEXT_SENSORS):
cv.ensure_list(text_sensor.TEXT_SENSOR_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),

View file

@ -8,7 +8,7 @@ CustomComponentConstructor = custom_component_ns.class_('CustomComponentConstruc
MULTI_CONF = True
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(CustomComponentConstructor),
cv.Required(CONF_LAMBDA): cv.lambda_,
cv.Required(CONF_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_COMPONENTS): cv.ensure_list(cv.Schema({
cv.GenerateID(): cv.declare_id(cg.Component)
}).extend(cv.COMPONENT_SCHEMA)),

View file

@ -13,7 +13,7 @@ CONFIG_SCHEMA = cv.All(sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).e
cv.Optional(CONF_ADDRESS): cv.hex_int,
cv.Optional(CONF_INDEX): cv.positive_int,
cv.Optional(CONF_RESOLUTION, default=12): cv.All(cv.int_, cv.Range(min=9, max=12)),
cv.Optional(CONF_RESOLUTION, default=12): cv.int_range(min=9, max=12),
}), cv.has_exactly_one_key(CONF_ADDRESS, CONF_INDEX))

View file

@ -55,7 +55,7 @@ CONF_BRIGHTNESS = 'brightness'
CONF_SATURATION = 'saturation'
CONF_TEST_PATTERN = 'test_pattern'
camera_range_param = cv.All(cv.int_, cv.Range(min=-2, max=2))
camera_range_param = cv.int_range(min=-2, max=2)
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(ESP32Camera),
@ -81,7 +81,7 @@ CONFIG_SCHEMA = cv.Schema({
cv.Optional(CONF_IDLE_FRAMERATE, default='0.1 fps'): cv.All(cv.framerate,
cv.Range(min=0, max=1)),
cv.Optional(CONF_RESOLUTION, default='640X480'): cv.enum(FRAME_SIZES, upper=True),
cv.Optional(CONF_JPEG_QUALITY, default=10): cv.All(cv.int_, cv.Range(min=10, max=63)),
cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=10, max=63),
cv.Optional(CONF_CONTRAST, default=0): camera_range_param,
cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param,
cv.Optional(CONF_SATURATION, default=0): camera_range_param,

View file

@ -11,7 +11,7 @@
namespace esphome {
namespace esp8266_pwm {
static const char *TAG = "something.something";
static const char *TAG = "esp8266_pwm";
void ESP8266PWM::setup() {
ESP_LOGCONFIG(TAG, "Setting up ESP8266 PWM Output...");

View file

@ -1,8 +1,7 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import light, power_supply
from esphome.const import CONF_OUTPUT_ID, CONF_NUM_LEDS, CONF_RGB_ORDER, CONF_MAX_REFRESH_RATE, \
CONF_POWER_SUPPLY
from esphome.components import light
from esphome.const import CONF_OUTPUT_ID, CONF_NUM_LEDS, CONF_RGB_ORDER, CONF_MAX_REFRESH_RATE
from esphome.core import coroutine
fastled_base_ns = cg.esphome_ns.namespace('fastled_base')
@ -24,8 +23,6 @@ BASE_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend({
cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int,
cv.Optional(CONF_RGB_ORDER): cv.one_of(*RGB_ORDERS, upper=True),
cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
cv.Optional(CONF_POWER_SUPPLY): cv.use_id(power_supply.PowerSupply),
}).extend(cv.COMPONENT_SCHEMA)
@ -37,10 +34,6 @@ def new_fastled_light(config):
if CONF_MAX_REFRESH_RATE in config:
cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE]))
if CONF_POWER_SUPPLY in config:
var_ = yield cg.get_variable(config[CONF_POWER_SUPPLY])
cg.add(var.set_power_supply(var_))
yield light.register_light(var, config)
cg.add_library('FastLED', '3.2.0')
yield var

View file

@ -33,27 +33,6 @@ void FastLEDLightOutput::loop() {
this->mark_shown_();
ESP_LOGVV(TAG, "Writing RGB values to bus...");
#ifdef USE_POWER_SUPPLY
if (this->power_supply_ != nullptr) {
bool is_on = false;
for (int i = 0; i < this->num_leds_; i++) {
if (bool(this->leds_[i])) {
is_on = true;
break;
}
}
if (is_on && !this->has_requested_high_power_) {
this->power_supply_->request_high_power();
this->has_requested_high_power_ = true;
}
if (!is_on && this->has_requested_high_power_) {
this->power_supply_->unrequest_high_power();
this->has_requested_high_power_ = false;
}
}
#endif
this->controller_->showLeds();
}

View file

@ -4,10 +4,6 @@
#include "esphome/core/helpers.h"
#include "esphome/components/light/addressable_light.h"
#ifdef USE_POWER_SUPPLY
#include "esphome/components/power_supply/power_supply.h"
#endif
#define FASTLED_ESP8266_RAW_PIN_ORDER
#define FASTLED_ESP32_RAW_PIN_ORDER
#define FASTLED_RMT_BUILTIN_DRIVER true
@ -30,10 +26,6 @@ class FastLEDLightOutput : public Component, public light::AddressableLight {
/// Set a maximum refresh rate in µs as some lights do not like being updated too often.
void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; }
#ifdef USE_POWER_SUPPLY
void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_supply_ = power_supply; }
#endif
/// Add some LEDS, can only be called once.
CLEDController &add_leds(CLEDController *controller, int num_leds) {
this->controller_ = controller;
@ -242,10 +234,6 @@ class FastLEDLightOutput : public Component, public light::AddressableLight {
int num_leds_{0};
uint32_t last_refresh_{0};
optional<uint32_t> max_refresh_rate_{};
#ifdef USE_POWER_SUPPLY
power_supply::PowerSupply *power_supply_{nullptr};
bool has_requested_high_power_{false};
#endif
};
} // namespace fastled_base

View file

@ -70,7 +70,7 @@ FONT_SCHEMA = cv.Schema({
cv.Required(CONF_ID): cv.declare_id(Font),
cv.Required(CONF_FILE): validate_truetype_file,
cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs,
cv.Optional(CONF_SIZE, default=20): cv.All(cv.int_, cv.Range(min=1)),
cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1),
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
})

View file

@ -13,8 +13,8 @@ void HLW8012Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up HLW8012...");
this->sel_pin_->setup();
this->sel_pin_->digital_write(this->current_mode_);
this->cf_store_.setup(this->cf_pin_);
this->cf1_store_.setup(this->cf1_pin_);
this->cf_store_.pulse_counter_setup(this->cf_pin_);
this->cf1_store_.pulse_counter_setup(this->cf1_pin_);
}
void HLW8012Component::dump_config() {
ESP_LOGCONFIG(TAG, "HLW8012:");
@ -32,18 +32,18 @@ void HLW8012Component::dump_config() {
float HLW8012Component::get_setup_priority() const { return setup_priority::DATA; }
void HLW8012Component::update() {
// HLW8012 has 50% duty cycle
const uint32_t last_rise_cf = this->cf_store_.get_last_rise();
const uint32_t last_rise_cf1 = this->cf1_store_.get_last_rise();
const uint32_t now = micros();
float full_cycle_cf = this->cf_store_.get_pulse_width_s() * 2;
float full_cycle_cf1 = this->cf1_store_.get_pulse_width_s() * 2;
float cf_hz = 0.0f, cf1_hz = 0.0f;
auto update_interval_micros = static_cast<uint32_t>(this->update_interval_ * 1e3f);
if (full_cycle_cf != 0.0f && now - last_rise_cf < update_interval_micros * 3)
cf_hz = 1.0f / full_cycle_cf;
if (full_cycle_cf1 != 0.0f && now - last_rise_cf1 < update_interval_micros * 3)
cf1_hz = 1.0f / full_cycle_cf1;
pulse_counter::pulse_counter_t raw_cf = this->cf_store_.read_raw_value();
pulse_counter::pulse_counter_t raw_cf1 = this->cf1_store_.read_raw_value();
float cf_hz = raw_cf / (this->get_update_interval() / 1000.0f);
if (raw_cf <= 1) {
// don't count single pulse as power
cf_hz = 0.0f;
}
float cf1_hz = raw_cf1 / (this->get_update_interval() / 1000.0f);
if (raw_cf1 <= 1) {
// don't count single pulse as anything
cf1_hz = 0.0f;
}
if (this->nth_value_++ < 2) {
return;

View file

@ -3,7 +3,7 @@
#include "esphome/core/component.h"
#include "esphome/core/esphal.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/pulse_width/pulse_width.h"
#include "esphome/components/pulse_counter/pulse_counter_sensor.h"
namespace esphome {
namespace hlw8012 {
@ -34,9 +34,9 @@ class HLW8012Component : public PollingComponent {
float voltage_divider_{2351};
GPIOPin *sel_pin_;
GPIOPin *cf_pin_;
pulse_width::PulseWidthSensorStore cf_store_;
pulse_counter::PulseCounterStorage cf_store_;
GPIOPin *cf1_pin_;
pulse_width::PulseWidthSensorStore cf1_store_;
pulse_counter::PulseCounterStorage cf1_store_;
sensor::Sensor *voltage_sensor_{nullptr};
sensor::Sensor *current_sensor_{nullptr};
sensor::Sensor *power_sensor_{nullptr};

View file

@ -6,7 +6,7 @@ from esphome.const import CONF_CHANGE_MODE_EVERY, CONF_CURRENT, \
CONF_CURRENT_RESISTOR, CONF_ID, CONF_POWER, CONF_SEL_PIN, CONF_VOLTAGE, CONF_VOLTAGE_DIVIDER, \
ICON_FLASH, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT
AUTO_LOAD = ['pulse_width']
AUTO_LOAD = ['pulse_counter']
hlw8012_ns = cg.esphome_ns.namespace('hlw8012')
HLW8012Component = hlw8012_ns.class_('HLW8012Component', cg.PollingComponent)

View file

@ -33,7 +33,7 @@ CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend({
cv.Required(CONF_TIME_UNIT): cv.enum(INTEGRATION_TIMES, lower=True),
cv.Optional(CONF_INTEGRATION_METHOD, default='trapezoid'):
cv.enum(INTEGRATION_METHODS, lower=True),
cv.Optional(CONF_RESTORE, default=True): cv.boolean,
cv.Optional(CONF_RESTORE, default=False): cv.boolean,
}).extend(cv.COMPONENT_SCHEMA)

View file

@ -1,20 +1,54 @@
import math
from esphome import pins
from esphome.components import output
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import APB_CLOCK_FREQ, CONF_BIT_DEPTH, CONF_CHANNEL, CONF_FREQUENCY, \
from esphome.const import CONF_BIT_DEPTH, CONF_CHANNEL, CONF_FREQUENCY, \
CONF_ID, CONF_PIN, ESP_PLATFORM_ESP32
ESP_PLATFORMS = [ESP_PLATFORM_ESP32]
def calc_max_frequency(bit_depth):
return 80e6 / (2**bit_depth)
def calc_min_frequency(bit_depth):
# LEDC_DIV_NUM_HSTIMER is 15-bit unsigned integer
# lower 8 bits represent fractional part
max_div_num = ((1 << 16) - 1) / 256.0
return 80e6 / (max_div_num * (2**bit_depth))
def validate_frequency_bit_depth(obj):
frequency = obj[CONF_FREQUENCY]
bit_depth = obj[CONF_BIT_DEPTH]
max_freq = APB_CLOCK_FREQ / (2**bit_depth)
if CONF_BIT_DEPTH not in obj:
obj = obj.copy()
for bit_depth in range(15, 0, -1):
if calc_min_frequency(bit_depth) <= frequency <= calc_max_frequency(bit_depth):
obj[CONF_BIT_DEPTH] = bit_depth
break
else:
min_freq = min(calc_min_frequency(x) for x in range(1, 16))
max_freq = max(calc_max_frequency(x) for x in range(1, 16))
if frequency < min_freq:
raise cv.Invalid("This frequency setting is not possible, please choose a higher "
"frequency (at least {}Hz)".format(int(min_freq)))
if frequency > max_freq:
raise cv.Invalid('Maximum frequency for bit depth {} is {}Hz'.format(bit_depth, max_freq))
raise cv.Invalid("This frequency setting is not possible, please choose a lower "
"frequency (at most {}Hz)".format(int(max_freq)))
raise cv.Invalid("Invalid frequency!")
bit_depth = obj[CONF_BIT_DEPTH]
min_freq = calc_min_frequency(bit_depth)
max_freq = calc_max_frequency(bit_depth)
if frequency > max_freq:
raise cv.Invalid('Maximum frequency for bit depth {} is {}Hz. Please decrease the '
'bit_depth.'.format(bit_depth, int(math.floor(max_freq))))
if frequency < calc_min_frequency(bit_depth):
raise cv.Invalid('Minimum frequency for bit depth {} is {}Hz. Please increase the '
'bit_depth.'.format(bit_depth, int(math.ceil(min_freq))))
return obj
@ -25,8 +59,8 @@ CONFIG_SCHEMA = cv.All(output.FLOAT_OUTPUT_SCHEMA.extend({
cv.Required(CONF_ID): cv.declare_id(LEDCOutput),
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_FREQUENCY, default='1kHz'): cv.frequency,
cv.Optional(CONF_BIT_DEPTH, default=12): cv.All(cv.int_, cv.Range(min=1, max=15)),
cv.Optional(CONF_CHANNEL): cv.All(cv.int_, cv.Range(min=0, max=15))
cv.Optional(CONF_BIT_DEPTH): cv.int_range(min=1, max=15),
cv.Optional(CONF_CHANNEL): cv.int_range(min=0, max=15),
}).extend(cv.COMPONENT_SCHEMA), validate_frequency_bit_depth)

View file

@ -1,9 +1,9 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import mqtt
from esphome.components import mqtt, power_supply
from esphome.const import CONF_COLOR_CORRECT, \
CONF_DEFAULT_TRANSITION_LENGTH, CONF_EFFECTS, CONF_GAMMA_CORRECT, CONF_ID, \
CONF_INTERNAL, CONF_NAME, CONF_MQTT_ID
CONF_INTERNAL, CONF_NAME, CONF_MQTT_ID, CONF_POWER_SUPPLY
from esphome.core import coroutine, coroutine_with_priority
from .automation import light_control_to_code # noqa
from .effects import validate_effects, BINARY_EFFECTS, \
@ -35,6 +35,7 @@ ADDRESSABLE_LIGHT_SCHEMA = RGB_LIGHT_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(AddressableLightState),
cv.Optional(CONF_EFFECTS): validate_effects(ADDRESSABLE_EFFECTS),
cv.Optional(CONF_COLOR_CORRECT): cv.All([cv.percentage], cv.Length(min=3, max=4)),
cv.Optional(CONF_POWER_SUPPLY): cv.use_id(power_supply.PowerSupply),
})
@ -52,6 +53,10 @@ def setup_light_core_(light_var, output_var, config):
if CONF_COLOR_CORRECT in config:
cg.add(output_var.set_correction(*config[CONF_COLOR_CORRECT]))
if CONF_POWER_SUPPLY in config:
var_ = yield cg.get_variable(config[CONF_POWER_SUPPLY])
cg.add(output_var.set_power_supply(var_))
if CONF_MQTT_ID in config:
mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], light_var)
yield mqtt.register_mqtt_component(mqtt_, config)

View file

@ -1,9 +1,14 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "light_output.h"
#include "light_state.h"
#ifdef USE_POWER_SUPPLY
#include "esphome/components/power_supply/power_supply.h"
#endif
namespace esphome {
namespace light {
@ -30,6 +35,7 @@ struct ESPColor {
};
};
uint8_t raw[4];
uint32_t raw_32;
};
inline ESPColor() ALWAYS_INLINE : r(0), g(0), b(0), w(0) {} // NOLINT
inline ESPColor(uint8_t red, uint8_t green, uint8_t blue, uint8_t white) ALWAYS_INLINE : r(red),
@ -47,7 +53,7 @@ struct ESPColor {
this->b = rhs.b;
this->w = rhs.w;
}
inline bool is_on() ALWAYS_INLINE { return this->r != 0 || this->g != 0 || this->b != 0 || this->w != 0; }
inline bool is_on() ALWAYS_INLINE { return this->raw_32 != 0; }
inline ESPColor &operator=(const ESPColor &rhs) ALWAYS_INLINE {
this->r = rhs.r;
this->g = rhs.g;
@ -527,14 +533,32 @@ class AddressableLight : public LightOutput {
void setup_state(LightState *state) override { this->correction_.calculate_gamma_table(state->get_gamma_correct()); }
void schedule_show() { this->next_show_ = true; }
#ifdef USE_POWER_SUPPLY
void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_.set_parent(power_supply); }
#endif
protected:
bool should_show_() const { return this->effect_active_ || this->next_show_; }
void mark_shown_() { this->next_show_ = false; }
void mark_shown_() {
this->next_show_ = false;
#ifdef USE_POWER_SUPPLY
for (auto c : *this) {
if (c.get().is_on()) {
this->power_.request();
return;
}
}
this->power_.unrequest();
#endif
}
virtual ESPColorView get_view_internal(int32_t index) const = 0;
bool effect_active_{false};
bool next_show_{true};
ESPColorCorrection correction_{};
#ifdef USE_POWER_SUPPLY
power_supply::PowerSupplyRequester power_;
#endif
};
} // namespace light

View file

@ -12,8 +12,8 @@ MAX7219ComponentRef = MAX7219Component.operator('ref')
CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(MAX7219Component),
cv.Optional(CONF_NUM_CHIPS, default=1): cv.All(cv.uint8_t, cv.Range(min=1)),
cv.Optional(CONF_INTENSITY, default=15): cv.All(cv.uint8_t, cv.Range(min=0, max=15)),
cv.Optional(CONF_NUM_CHIPS, default=1): cv.int_range(min=1, max=255),
cv.Optional(CONF_INTENSITY, default=15): cv.int_range(min=0, max=15),
}).extend(cv.polling_component_schema('1s')).extend(spi.SPI_DEVICE_SCHEMA)

View file

@ -24,12 +24,12 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC
JsonArray &modes = root.createNestedArray("modes");
// sort array for nice UI in HA
if (traits.supports_mode(CLIMATE_MODE_AUTO))
modes.add("auto");
modes.add("off");
modes.add(climate_mode_to_string(CLIMATE_MODE_AUTO));
modes.add(climate_mode_to_string(CLIMATE_MODE_OFF));
if (traits.supports_mode(CLIMATE_MODE_COOL))
modes.add("cool");
modes.add(climate_mode_to_string(CLIMATE_MODE_COOL));
if (traits.supports_mode(CLIMATE_MODE_HEAT))
modes.add("heat");
modes.add(climate_mode_to_string(CLIMATE_MODE_HEAT));
if (traits.get_supports_two_point_target_temperature()) {
// temperature_low_command_topic
@ -60,6 +60,8 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC
// away_mode_state_topic
root["away_mode_state_topic"] = this->get_away_state_topic();
}
config.state_topic = false;
config.command_topic = false;
}
void MQTTClimateComponent::setup() {
auto traits = this->device_->get_traits();
@ -144,7 +146,7 @@ bool MQTTClimateComponent::publish_state_() {
if (!this->publish(this->get_mode_state_topic(), mode_s))
success = false;
int8_t accuracy = traits.get_temperature_accuracy_decimals();
if (traits.get_supports_current_temperature()) {
if (traits.get_supports_current_temperature() && !isnan(this->device_->current_temperature)) {
std::string payload = value_accuracy_to_string(this->device_->current_temperature, accuracy);
if (!this->publish(this->get_current_temperature_state_topic(), payload))
success = false;

View file

@ -13,8 +13,8 @@ CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(MY9231OutputComponent),
cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema,
cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_NUM_CHANNELS, default=6): cv.All(cv.int_, cv.Range(min=3, max=1020)),
cv.Optional(CONF_NUM_CHIPS, default=2): cv.All(cv.int_, cv.Range(min=1, max=255)),
cv.Optional(CONF_NUM_CHANNELS, default=6): cv.int_range(min=3, max=1020),
cv.Optional(CONF_NUM_CHIPS, default=2): cv.int_range(min=1, max=255),
cv.Optional(CONF_BIT_DEPTH, default=16): cv.one_of(8, 12, 14, 16, int=True),
}).extend(cv.COMPONENT_SCHEMA)

View file

@ -13,7 +13,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({
cv.GenerateID(CONF_MY9231_ID): cv.use_id(MY9231OutputComponent),
cv.Required(CONF_ID): cv.declare_id(Channel),
cv.Required(CONF_CHANNEL): cv.All(cv.int_, cv.Range(min=0, max=65535)),
cv.Required(CONF_CHANNEL): cv.uint16_t,
}).extend(cv.COMPONENT_SCHEMA)

View file

@ -1,9 +1,9 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import light, power_supply
from esphome.components import light
from esphome.const import CONF_CLOCK_PIN, CONF_DATA_PIN, CONF_METHOD, CONF_NUM_LEDS, CONF_PIN, \
CONF_POWER_SUPPLY, CONF_TYPE, CONF_VARIANT, CONF_OUTPUT_ID
CONF_TYPE, CONF_VARIANT, CONF_OUTPUT_ID
from esphome.core import CORE
neopixelbus_ns = cg.esphome_ns.namespace('neopixelbus')
@ -138,8 +138,6 @@ CONFIG_SCHEMA = cv.All(light.ADDRESSABLE_LIGHT_SCHEMA.extend({
cv.Optional(CONF_DATA_PIN): pins.output_pin,
cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int,
cv.Optional(CONF_POWER_SUPPLY): cv.use_id(power_supply.PowerSupply),
}).extend(cv.COMPONENT_SCHEMA), validate, validate_method_pin)
@ -162,8 +160,4 @@ def to_code(config):
cg.add(var.set_pixel_order(getattr(ESPNeoPixelOrder, config[CONF_TYPE])))
if CONF_POWER_SUPPLY in config:
var_ = yield cg.get_variable(config[CONF_POWER_SUPPLY])
cg.add(var.set_power_supply(var_))
cg.add_library('NeoPixelBus', '2.4.1')

View file

@ -9,10 +9,6 @@
#error The NeoPixelBus library requires at least arduino_core_version 2.4.x
#endif
#ifdef USE_POWER_SUPPLY
#include "esphome/components/power_supply/power_supply.h"
#endif
#include "NeoPixelBus.h"
namespace esphome {
@ -54,10 +50,6 @@ enum class ESPNeoPixelOrder {
template<typename T_METHOD, typename T_COLOR_FEATURE>
class NeoPixelBusLightOutputBase : public Component, public light::AddressableLight {
public:
#ifdef USE_POWER_SUPPLY
void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_supply_ = power_supply; }
#endif
NeoPixelBus<T_COLOR_FEATURE, T_METHOD> *get_controller() const { return this->controller_; }
void clear_effect_data() override {
@ -95,27 +87,6 @@ class NeoPixelBusLightOutputBase : public Component, public light::AddressableLi
this->mark_shown_();
this->controller_->Dirty();
#ifdef USE_POWER_SUPPLY
if (this->power_supply_ != nullptr) {
bool is_light_on = false;
for (int i = 0; i < this->size(); i++) {
if ((*this)[i].get().is_on()) {
is_light_on = true;
break;
}
}
if (is_light_on && !this->has_requested_high_power_) {
this->power_supply_->request_high_power();
this->has_requested_high_power_ = true;
}
if (!is_light_on && this->has_requested_high_power_) {
this->power_supply_->unrequest_high_power();
this->has_requested_high_power_ = false;
}
}
#endif
this->controller_->Show();
}
@ -135,10 +106,6 @@ class NeoPixelBusLightOutputBase : public Component, public light::AddressableLi
NeoPixelBus<T_COLOR_FEATURE, T_METHOD> *controller_{nullptr};
uint8_t *effect_data_{nullptr};
uint8_t rgb_offsets_[4]{0, 1, 2, 3};
#ifdef USE_POWER_SUPPLY
power_supply::PowerSupply *power_supply_{nullptr};
bool has_requested_high_power_{false};
#endif
};
template<typename T_METHOD, typename T_COLOR_FEATURE = NeoRgbFeature>

View file

@ -27,16 +27,13 @@ class BinaryOutput {
*
* @param power_supply The PowerSupplyComponent, set this to nullptr to disable the power supply.
*/
void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_supply_ = power_supply; }
void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_.set_parent(power_supply); }
#endif
/// Enable this binary output.
virtual void turn_on() {
#ifdef USE_POWER_SUPPLY
if (this->power_supply_ != nullptr && !this->has_requested_high_power_) {
this->power_supply_->request_high_power();
this->has_requested_high_power_ = true;
}
this->power_.request();
#endif
this->write_state(!this->inverted_);
}
@ -44,10 +41,7 @@ class BinaryOutput {
/// Disable this binary output.
virtual void turn_off() {
#ifdef USE_POWER_SUPPLY
if (this->power_supply_ != nullptr && this->has_requested_high_power_) {
this->power_supply_->unrequest_high_power();
this->has_requested_high_power_ = false;
}
this->power_.unrequest();
#endif
this->write_state(this->inverted_);
}
@ -62,8 +56,7 @@ class BinaryOutput {
bool inverted_{false};
#ifdef USE_POWER_SUPPLY
power_supply::PowerSupply *power_supply_{nullptr};
bool has_requested_high_power_{false};
power_supply::PowerSupplyRequester power_{};
#endif
};

View file

@ -24,15 +24,9 @@ void FloatOutput::set_level(float state) {
#ifdef USE_POWER_SUPPLY
if (state > 0.0f) { // ON
if (this->power_supply_ != nullptr && !this->has_requested_high_power_) {
this->power_supply_->request_high_power();
this->has_requested_high_power_ = true;
}
this->power_.request();
} else { // OFF
if (this->power_supply_ != nullptr && this->has_requested_high_power_) {
this->power_supply_->unrequest_high_power();
this->has_requested_high_power_ = false;
}
this->power_.unrequest();
}
#endif

View file

@ -13,7 +13,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({
cv.Required(CONF_ID): cv.declare_id(PCA9685Channel),
cv.GenerateID(CONF_PCA9685_ID): cv.use_id(PCA9685Output),
cv.Required(CONF_CHANNEL): cv.All(cv.Coerce(int), cv.Range(min=0, max=15)),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=15),
})

View file

@ -132,14 +132,20 @@ void PMSX003Component::parse_data_() {
break;
}
case PMSX003_TYPE_5003ST: {
uint16_t pm_1_0_concentration = this->get_16_bit_uint_(10);
uint16_t pm_2_5_concentration = this->get_16_bit_uint_(12);
uint16_t pm_10_0_concentration = this->get_16_bit_uint_(14);
uint16_t formaldehyde = this->get_16_bit_uint_(28);
float temperature = this->get_16_bit_uint_(30) / 10.0f;
float humidity = this->get_16_bit_uint_(32) / 10.0f;
ESP_LOGD(TAG, "Got PM2.5 Concentration: %u µg/m^3, Temperature: %.1f°C, Humidity: %.1f%% Formaldehyde: %u µg/m^3",
pm_2_5_concentration, temperature, humidity, formaldehyde);
if (this->pm_1_0_sensor_ != nullptr)
this->pm_1_0_sensor_->publish_state(pm_1_0_concentration);
if (this->pm_2_5_sensor_ != nullptr)
this->pm_2_5_sensor_->publish_state(pm_2_5_concentration);
if (this->pm_10_0_sensor_ != nullptr)
this->pm_10_0_sensor_->publish_state(pm_10_0_concentration);
if (this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(temperature);
if (this->humidity_sensor_ != nullptr)

View file

@ -24,9 +24,9 @@ PMSX003_TYPES = {
}
SENSORS_TO_TYPE = {
CONF_PM_1_0: [CONF_PMSX003],
CONF_PM_1_0: [CONF_PMSX003, CONF_PMS5003ST],
CONF_PM_2_5: [CONF_PMSX003, CONF_PMS5003T, CONF_PMS5003ST],
CONF_PM_10_0: [CONF_PMSX003],
CONF_PM_10_0: [CONF_PMSX003, CONF_PMS5003ST],
CONF_TEMPERATURE: [CONF_PMS5003T, CONF_PMS5003ST],
CONF_HUMIDITY: [CONF_PMS5003T, CONF_PMS5003ST],
CONF_FORMALDEHYDE: [CONF_PMS5003ST],

View file

@ -42,7 +42,7 @@ void PowerSupply::request_high_power() {
void PowerSupply::unrequest_high_power() {
this->active_requests_--;
if (this->active_requests_ < 0) {
// if asserts are disabled we're just going to use 0 as our now counter.
// we're just going to use 0 as our now counter.
this->active_requests_ = 0;
}

View file

@ -39,5 +39,26 @@ class PowerSupply : public Component {
int16_t active_requests_{0}; // use signed integer to make catching negative requests easier.
};
class PowerSupplyRequester {
public:
void set_parent(PowerSupply *parent) { parent_ = parent; }
void request() {
if (!this->requested_ && this->parent_ != nullptr) {
this->parent_->request_high_power();
this->requested_ = true;
}
}
void unrequest() {
if (this->requested_ && this->parent_ != nullptr) {
this->parent_->unrequest_high_power();
this->requested_ = false;
}
}
protected:
PowerSupply *parent_{nullptr};
bool requested_{false};
};
} // namespace power_supply
} // namespace esphome

View file

@ -28,7 +28,9 @@ void ICACHE_RAM_ATTR PulseCounterStorage::gpio_intr(PulseCounterStorage *arg) {
break;
}
}
bool PulseCounterStorage::pulse_counter_setup() {
bool PulseCounterStorage::pulse_counter_setup(GPIOPin *pin) {
this->pin = pin;
this->pin->setup();
this->isr_pin = this->pin->to_isr();
this->pin->attach_interrupt(PulseCounterStorage::gpio_intr, this, CHANGE);
return true;
@ -42,7 +44,9 @@ pulse_counter_t PulseCounterStorage::read_raw_value() {
#endif
#ifdef ARDUINO_ARCH_ESP32
bool PulseCounterStorage::pulse_counter_setup() {
bool PulseCounterStorage::pulse_counter_setup(GPIOPin *pin) {
this->pin = pin;
this->pin->setup();
this->pcnt_unit = next_pcnt_unit;
next_pcnt_unit = pcnt_unit_t(int(next_pcnt_unit) + 1); // NOLINT
@ -133,9 +137,7 @@ pulse_counter_t PulseCounterStorage::read_raw_value() {
void PulseCounterSensor::setup() {
ESP_LOGCONFIG(TAG, "Setting up pulse counter '%s'...", this->name_.c_str());
this->pin_->setup();
this->storage_.pin = this->pin_;
if (!this->storage_.pulse_counter_setup()) {
if (!this->storage_.pulse_counter_setup(this->pin_)) {
this->mark_failed();
return;
}

View file

@ -25,7 +25,7 @@ using pulse_counter_t = int32_t;
#endif
struct PulseCounterStorage {
bool pulse_counter_setup();
bool pulse_counter_setup(GPIOPin *pin);
pulse_counter_t read_raw_value();
static void gpio_intr(PulseCounterStorage *arg);
@ -42,9 +42,9 @@ struct PulseCounterStorage {
#ifdef ARDUINO_ARCH_ESP8266
ISRInternalGPIOPin *isr_pin;
#endif
PulseCounterCountMode rising_edge_mode{};
PulseCounterCountMode falling_edge_mode{};
uint32_t filter_us{};
PulseCounterCountMode rising_edge_mode{PULSE_COUNTER_INCREMENT};
PulseCounterCountMode falling_edge_mode{PULSE_COUNTER_DISABLE};
uint32_t filter_us{0};
pulse_counter_t last_value{0};
};

View file

@ -410,7 +410,7 @@ def rc5_action(var, config, args):
RC_SWITCH_TIMING_SCHEMA = cv.All([cv.uint8_t], cv.Length(min=2, max=2))
RC_SWITCH_PROTOCOL_SCHEMA = cv.Any(
cv.All(cv.Coerce(int), cv.Range(min=1, max=7)),
cv.int_range(min=1, max=7),
cv.Schema({
cv.Required(CONF_PULSE_LENGTH): cv.uint32_t,
cv.Optional(CONF_SYNC, default=[1, 31]): RC_SWITCH_TIMING_SCHEMA,
@ -457,22 +457,22 @@ RC_SWITCH_TYPE_A_SCHEMA = cv.Schema({
cv.Optional(CONF_PROTOCOL, default=1): RC_SWITCH_PROTOCOL_SCHEMA,
})
RC_SWITCH_TYPE_B_SCHEMA = cv.Schema({
cv.Required(CONF_ADDRESS): cv.All(cv.uint8_t, cv.Range(min=1, max=4)),
cv.Required(CONF_CHANNEL): cv.All(cv.uint8_t, cv.Range(min=1, max=4)),
cv.Required(CONF_ADDRESS): cv.int_range(min=1, max=4),
cv.Required(CONF_CHANNEL): cv.int_range(min=1, max=4),
cv.Required(CONF_STATE): cv.boolean,
cv.Optional(CONF_PROTOCOL, default=1): RC_SWITCH_PROTOCOL_SCHEMA,
})
RC_SWITCH_TYPE_C_SCHEMA = cv.Schema({
cv.Required(CONF_FAMILY): cv.one_of('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
'l', 'm', 'n', 'o', 'p', lower=True),
cv.Required(CONF_GROUP): cv.All(cv.uint8_t, cv.Range(min=1, max=4)),
cv.Required(CONF_DEVICE): cv.All(cv.uint8_t, cv.Range(min=1, max=4)),
cv.Required(CONF_GROUP): cv.int_range(min=1, max=4),
cv.Required(CONF_DEVICE): cv.int_range(min=1, max=4),
cv.Required(CONF_STATE): cv.boolean,
cv.Optional(CONF_PROTOCOL, default=1): RC_SWITCH_PROTOCOL_SCHEMA,
})
RC_SWITCH_TYPE_D_SCHEMA = cv.Schema({
cv.Required(CONF_GROUP): cv.one_of('a', 'b', 'c', 'd', lower=True),
cv.Required(CONF_DEVICE): cv.All(cv.uint8_t, cv.Range(min=1, max=3)),
cv.Required(CONF_DEVICE): cv.int_range(min=1, max=3),
cv.Required(CONF_STATE): cv.boolean,
cv.Optional(CONF_PROTOCOL, default=1): RC_SWITCH_PROTOCOL_SCHEMA,
})

View file

@ -148,7 +148,7 @@ def exponential_moving_average_filter_to_code(config, filter_id):
yield cg.new_Pvariable(filter_id, config[CONF_ALPHA], config[CONF_SEND_EVERY])
@FILTER_REGISTRY.register('lambda', LambdaFilter, cv.lambda_)
@FILTER_REGISTRY.register('lambda', LambdaFilter, cv.returning_lambda)
def lambda_filter_to_code(config, filter_id):
lambda_ = yield cg.process_lambda(config, [(float, 'x')],
return_type=cg.optional.template(float))

View file

@ -88,10 +88,8 @@ std::string Sensor::unique_id() { return ""; }
void Sensor::internal_send_state_to_frontend(float state) {
this->has_state_ = true;
this->state = state;
if (this->filter_list_ != nullptr) {
ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state,
this->get_unit_of_measurement().c_str(), this->get_accuracy_decimals());
}
this->callback_.call(state);
}
bool Sensor::has_state() const { return this->has_state_; }

View file

@ -0,0 +1,103 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.components import time
from esphome.const import CONF_TIME_ID, CONF_ID, CONF_TRIGGER_ID
sun_ns = cg.esphome_ns.namespace('sun')
Sun = sun_ns.class_('Sun')
SunTrigger = sun_ns.class_('SunTrigger', cg.PollingComponent, automation.Trigger.template())
SunCondition = sun_ns.class_('SunCondition', automation.Condition)
CONF_SUN_ID = 'sun_id'
CONF_LATITUDE = 'latitude'
CONF_LONGITUDE = 'longitude'
CONF_ELEVATION = 'elevation'
CONF_ON_SUNRISE = 'on_sunrise'
CONF_ON_SUNSET = 'on_sunset'
ELEVATION_MAP = {
'sunrise': 0.0,
'sunset': 0.0,
'civil': -6.0,
'nautical': -12.0,
'astronomical': -18.0,
}
def elevation(value):
if isinstance(value, str):
try:
value = ELEVATION_MAP[cv.one_of(*ELEVATION_MAP, lower=True, space='_')]
except cv.Invalid:
pass
value = cv.angle(value)
return cv.float_range(min=-180, max=180)(value)
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(Sun),
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
cv.Required(CONF_LATITUDE): cv.float_range(min=-90, max=90),
cv.Required(CONF_LONGITUDE): cv.float_range(min=-180, max=180),
cv.Optional(CONF_ON_SUNRISE): automation.validate_automation({
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SunTrigger),
cv.Optional(CONF_ELEVATION, default=0.0): elevation,
}),
cv.Optional(CONF_ON_SUNSET): automation.validate_automation({
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SunTrigger),
cv.Optional(CONF_ELEVATION, default=0.0): elevation,
}),
})
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
time_ = yield cg.get_variable(config[CONF_TIME_ID])
cg.add(var.set_time(time_))
cg.add(var.set_latitude(config[CONF_LATITUDE]))
cg.add(var.set_longitude(config[CONF_LONGITUDE]))
for conf in config.get(CONF_ON_SUNRISE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
yield cg.register_component(trigger, conf)
yield cg.register_parented(trigger, var)
cg.add(trigger.set_sunrise(True))
cg.add(trigger.set_elevation(conf[CONF_ELEVATION]))
yield automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_SUNSET, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
yield cg.register_component(trigger, conf)
yield cg.register_parented(trigger, var)
cg.add(trigger.set_sunrise(False))
cg.add(trigger.set_elevation(conf[CONF_ELEVATION]))
yield automation.build_automation(trigger, [], conf)
@automation.register_condition('sun.is_above_horizon', SunCondition, cv.Schema({
cv.GenerateID(): cv.use_id(Sun),
cv.Optional(CONF_ELEVATION, default=0): cv.templatable(elevation),
}))
def sun_above_horizon_to_code(config, condition_id, template_arg, args):
var = cg.new_Pvariable(condition_id, template_arg)
yield cg.register_parented(var, config[CONF_ID])
templ = yield cg.templatable(config[CONF_ELEVATION], args, cg.double)
cg.add(var.set_elevation(templ))
cg.add(var.set_above(True))
yield var
@automation.register_condition('sun.is_below_horizon', SunCondition, cv.Schema({
cv.GenerateID(): cv.use_id(Sun),
cv.Optional(CONF_ELEVATION, default=0): cv.templatable(elevation),
}))
def sun_below_horizon_to_code(config, condition_id, template_arg, args):
var = cg.new_Pvariable(condition_id, template_arg)
yield cg.register_parented(var, config[CONF_ID])
templ = yield cg.templatable(config[CONF_ELEVATION], args, cg.double)
cg.add(var.set_elevation(templ))
cg.add(var.set_above(False))
yield var

View file

@ -0,0 +1,30 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import UNIT_DEGREES, ICON_WEATHER_SUNSET, CONF_ID, CONF_TYPE
from .. import sun_ns, CONF_SUN_ID, Sun
DEPENDENCIES = ['sun']
SunSensor = sun_ns.class_('SunSensor', sensor.Sensor, cg.PollingComponent)
SensorType = sun_ns.enum('SensorType')
TYPES = {
'elevation': SensorType.SUN_SENSOR_ELEVATION,
'azimuth': SensorType.SUN_SENSOR_AZIMUTH,
}
CONFIG_SCHEMA = sensor.sensor_schema(UNIT_DEGREES, ICON_WEATHER_SUNSET, 1).extend({
cv.GenerateID(): cv.declare_id(SunSensor),
cv.GenerateID(CONF_SUN_ID): cv.use_id(Sun),
cv.Required(CONF_TYPE): cv.enum(TYPES, lower=True),
}).extend(cv.polling_component_schema('60s'))
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield cg.register_component(var, config)
yield sensor.register_sensor(var, config)
cg.add(var.set_type(config[CONF_TYPE]))
paren = yield cg.get_variable(config[CONF_SUN_ID])
cg.add(var.set_parent(paren))

View file

@ -0,0 +1,12 @@
#include "sun_sensor.h"
#include "esphome/core/log.h"
namespace esphome {
namespace sun {
static const char *TAG = "sun.sensor";
void SunSensor::dump_config() { LOG_SENSOR("", "Sun Sensor", this); }
} // namespace sun
} // namespace esphome

View file

@ -0,0 +1,41 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sun/sun.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace sun {
enum SensorType {
SUN_SENSOR_ELEVATION,
SUN_SENSOR_AZIMUTH,
};
class SunSensor : public sensor::Sensor, public PollingComponent {
public:
void set_parent(Sun *parent) { parent_ = parent; }
void set_type(SensorType type) { type_ = type; }
void dump_config() override;
void update() override {
double val;
switch (this->type_) {
case SUN_SENSOR_ELEVATION:
val = this->parent_->elevation();
break;
case SUN_SENSOR_AZIMUTH:
val = this->parent_->azimuth();
break;
default:
return;
}
this->publish_state(val);
}
protected:
sun::Sun *parent_;
SensorType type_;
};
} // namespace sun
} // namespace esphome

View file

@ -0,0 +1,168 @@
#include "sun.h"
#include "esphome/core/log.h"
namespace esphome {
namespace sun {
static const char *TAG = "sun";
#undef PI
/* Usually, ESPHome uses single-precision floating point values
* because those tend to be accurate enough and are more efficient.
*
* However, some of the data in this class has to be quite accurate, so double is
* used everywhere.
*/
static const double PI = 3.141592653589793;
static const double TAU = 6.283185307179586;
static const double TO_RADIANS = PI / 180.0;
static const double TO_DEGREES = 180.0 / PI;
static const double EARTH_TILT = 23.44 * TO_RADIANS;
optional<time::ESPTime> Sun::sunrise(double elevation) {
auto time = this->time_->now();
if (!time.is_valid())
return {};
double sun_time = this->sun_time_for_elevation_(time.day_of_year, elevation, true);
if (isnan(sun_time))
return {};
uint32_t epoch = this->calc_epoch_(time, sun_time);
return time::ESPTime::from_epoch_local(epoch);
}
optional<time::ESPTime> Sun::sunset(double elevation) {
auto time = this->time_->now();
if (!time.is_valid())
return {};
double sun_time = this->sun_time_for_elevation_(time.day_of_year, elevation, false);
if (isnan(sun_time))
return {};
uint32_t epoch = this->calc_epoch_(time, sun_time);
return time::ESPTime::from_epoch_local(epoch);
}
double Sun::elevation() {
auto time = this->current_sun_time_();
if (isnan(time))
return NAN;
return this->elevation_(time);
}
double Sun::azimuth() {
auto time = this->current_sun_time_();
if (isnan(time))
return NAN;
return this->azimuth_(time);
}
double Sun::sun_declination_(double sun_time) {
double n = sun_time - 1.0;
// maximum declination
const double tot = -sin(EARTH_TILT);
// eccentricity of the earth's orbit (ellipse)
double eccentricity = 0.0167;
// days since perihelion (January 3rd)
double days_since_perihelion = n - 2;
// days since december solstice (december 22)
double days_since_december_solstice = n + 10;
const double c = TAU / 365.24;
double v = cos(c * days_since_december_solstice + 2 * eccentricity * sin(c * days_since_perihelion));
// Make sure value is in range (double error may lead to results slightly larger than 1)
double x = clamp(tot * v, 0, 1);
return asin(x);
}
double Sun::elevation_ratio_(double sun_time) {
double decl = this->sun_declination_(sun_time);
double hangle = this->hour_angle_(sun_time);
double a = sin(this->latitude_rad_()) * sin(decl);
double b = cos(this->latitude_rad_()) * cos(decl) * cos(hangle);
double val = clamp(a + b, -1.0, 1.0);
return val;
}
double Sun::latitude_rad_() { return this->latitude_ * TO_RADIANS; }
double Sun::hour_angle_(double sun_time) {
double time_of_day = fmod(sun_time, 1.0) * 24.0;
return -PI * (time_of_day - 12) / 12;
}
double Sun::elevation_(double sun_time) { return this->elevation_rad_(sun_time) * TO_DEGREES; }
double Sun::elevation_rad_(double sun_time) { return asin(this->elevation_ratio_(sun_time)); }
double Sun::zenith_rad_(double sun_time) { return acos(this->elevation_ratio_(sun_time)); }
double Sun::azimuth_rad_(double sun_time) {
double hangle = -this->hour_angle_(sun_time);
double decl = this->sun_declination_(sun_time);
double zen = this->zenith_rad_(sun_time);
double nom = cos(zen) * sin(this->latitude_rad_()) - sin(decl);
double denom = sin(zen) * cos(this->latitude_rad_());
double v = clamp(nom / denom, -1.0, 1.0);
double az = PI - acos(v);
if (hangle > 0)
az = -az;
if (az < 0)
az += TAU;
return az;
}
double Sun::azimuth_(double sun_time) { return this->azimuth_rad_(sun_time) * TO_DEGREES; }
double Sun::calc_sun_time_(const time::ESPTime &time) {
// Time as seen at 0° longitude
if (!time.is_valid())
return NAN;
double base = (time.day_of_year + time.hour / 24.0 + time.minute / 24.0 / 60.0 + time.second / 24.0 / 60.0 / 60.0);
// Add longitude correction
double add = this->longitude_ / 360.0;
return base + add;
}
uint32_t Sun::calc_epoch_(time::ESPTime base, double sun_time) {
sun_time -= this->longitude_ / 360.0;
base.day_of_year = uint32_t(floor(sun_time));
sun_time = (sun_time - base.day_of_year) * 24.0;
base.hour = uint32_t(floor(sun_time));
sun_time = (sun_time - base.hour) * 60.0;
base.minute = uint32_t(floor(sun_time));
sun_time = (sun_time - base.minute) * 60.0;
base.second = uint32_t(floor(sun_time));
base.recalc_timestamp_utc(true);
return base.timestamp;
}
double Sun::sun_time_for_elevation_(int32_t day_of_year, double elevation, bool rising) {
// Use binary search, newton's method would be better but binary search already
// converges quite well (19 cycles) and much simpler. Function is guaranteed to be
// monotonous.
double lo, hi;
if (rising) {
lo = day_of_year + 0.0;
hi = day_of_year + 0.5;
} else {
lo = day_of_year + 1.0;
hi = day_of_year + 0.5;
}
double min_elevation = this->elevation_(lo);
double max_elevation = this->elevation_(hi);
if (elevation < min_elevation || elevation > max_elevation)
return NAN;
// Accuracy: 0.1s
const double accuracy = 1.0 / (24.0 * 60.0 * 60.0 * 10.0);
while (fabs(hi - lo) > accuracy) {
double mid = (lo + hi) / 2.0;
double value = this->elevation_(mid) - elevation;
if (value < 0) {
lo = mid;
} else if (value > 0) {
hi = mid;
} else {
lo = hi = mid;
break;
}
}
return (lo + hi) / 2.0;
}
} // namespace sun
} // namespace esphome

View file

@ -0,0 +1,137 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/automation.h"
#include "esphome/components/time/real_time_clock.h"
namespace esphome {
namespace sun {
class Sun {
public:
void set_time(time::RealTimeClock *time) { time_ = time; }
time::RealTimeClock *get_time() const { return time_; }
void set_latitude(double latitude) { latitude_ = latitude; }
void set_longitude(double longitude) { longitude_ = longitude; }
optional<time::ESPTime> sunrise(double elevation = 0.0);
optional<time::ESPTime> sunset(double elevation = 0.0);
double elevation();
double azimuth();
protected:
double current_sun_time_() { return this->calc_sun_time_(this->time_->utcnow()); }
/** Calculate the declination of the sun in rad.
*
* See https://en.wikipedia.org/wiki/Position_of_the_Sun#Declination_of_the_Sun_as_seen_from_Earth
*
* Accuracy: ±0.2°
*
* @param sun_time The day of the year, 1 means January 1st. See calc_sun_time_.
* @return Sun declination in degrees
*/
double sun_declination_(double sun_time);
double elevation_ratio_(double sun_time);
/** Calculate the hour angle based on the sun time of day in hours.
*
* Positive in morning, 0 at noon, negative in afternoon.
*
* @param sun_time Sun time, see calc_sun_time_.
* @return Hour angle in rad.
*/
double hour_angle_(double sun_time);
double elevation_(double sun_time);
double elevation_rad_(double sun_time);
double zenith_rad_(double sun_time);
double azimuth_rad_(double sun_time);
double azimuth_(double sun_time);
/** Return the sun time given by the time_ object.
*
* Sun time is defined as doubleing point day of year.
* Integer part encodes the day of the year (1=January 1st)
* Decimal part encodes time of day (1/24 = 1 hour)
*/
double calc_sun_time_(const time::ESPTime &time);
uint32_t calc_epoch_(time::ESPTime base, double sun_time);
/** Calculate the sun time of day
*
* @param day_of_year
* @param elevation
* @param rising
* @return
*/
double sun_time_for_elevation_(int32_t day_of_year, double elevation, bool rising);
double latitude_rad_();
time::RealTimeClock *time_;
/// Latitude in degrees, range: -90 to 90.
double latitude_;
/// Longitude in degrees, range: -180 to 180.
double longitude_;
};
class SunTrigger : public Trigger<>, public PollingComponent, public Parented<Sun> {
public:
SunTrigger() : PollingComponent(1000) {}
void set_sunrise(bool sunrise) { sunrise_ = sunrise; }
void set_elevation(double elevation) { elevation_ = elevation; }
void update() override {
double current = this->parent_->elevation();
if (isnan(current))
return;
bool crossed;
if (this->sunrise_) {
crossed = this->last_elevation_ <= this->elevation_ && this->elevation_ < current;
} else {
crossed = this->last_elevation_ >= this->elevation_ && this->elevation_ > current;
}
if (crossed) {
this->trigger();
}
this->last_elevation_ = current;
}
protected:
bool sunrise_;
double last_elevation_;
double elevation_;
};
template<typename... Ts> class SunCondition : public Condition<Ts...>, public Parented<Sun> {
public:
TEMPLATABLE_VALUE(double, elevation);
void set_above(bool above) { above_ = above; }
bool check(Ts... x) override {
double elevation = this->elevation_.value(x...);
double current = this->parent_->elevation();
if (this->above_)
return current > elevation;
else
return current < elevation;
}
protected:
bool above_;
};
} // namespace sun
} // namespace esphome

View file

@ -0,0 +1,45 @@
from esphome.components import text_sensor
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import CONF_ICON, ICON_WEATHER_SUNSET_DOWN, ICON_WEATHER_SUNSET_UP, CONF_TYPE, \
CONF_ID, CONF_FORMAT
from .. import sun_ns, CONF_SUN_ID, Sun, CONF_ELEVATION, elevation
DEPENDENCIES = ['sun']
SunTextSensor = sun_ns.class_('SunTextSensor', text_sensor.TextSensor, cg.PollingComponent)
SUN_TYPES = {
'sunset': False,
'sunrise': True,
}
def validate_optional_icon(config):
if CONF_ICON not in config:
config = config.copy()
config[CONF_ICON] = {
'sunset': ICON_WEATHER_SUNSET_DOWN,
'sunrise': ICON_WEATHER_SUNSET_UP,
}[config[CONF_TYPE]]
return config
CONFIG_SCHEMA = cv.All(text_sensor.TEXT_SENSOR_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(SunTextSensor),
cv.GenerateID(CONF_SUN_ID): cv.use_id(Sun),
cv.Required(CONF_TYPE): cv.one_of(*SUN_TYPES, lower=True),
cv.Optional(CONF_ELEVATION, default=0): elevation,
cv.Optional(CONF_FORMAT, default='%X'): cv.string_strict,
}).extend(cv.polling_component_schema('60s')), validate_optional_icon)
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield cg.register_component(var, config)
yield text_sensor.register_text_sensor(var, config)
paren = yield cg.get_variable(config[CONF_SUN_ID])
cg.add(var.set_parent(paren))
cg.add(var.set_sunrise(SUN_TYPES[config[CONF_TYPE]]))
cg.add(var.set_elevation(config[CONF_ELEVATION]))
cg.add(var.set_format(config[CONF_FORMAT]))

View file

@ -0,0 +1,12 @@
#include "sun_text_sensor.h"
#include "esphome/core/log.h"
namespace esphome {
namespace sun {
static const char *TAG = "sun.text_sensor";
void SunTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Sun Text Sensor", this); }
} // namespace sun
} // namespace esphome

View file

@ -0,0 +1,41 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sun/sun.h"
#include "esphome/components/text_sensor/text_sensor.h"
namespace esphome {
namespace sun {
class SunTextSensor : public text_sensor::TextSensor, public PollingComponent {
public:
void set_parent(Sun *parent) { parent_ = parent; }
void set_elevation(double elevation) { elevation_ = elevation; }
void set_sunrise(bool sunrise) { sunrise_ = sunrise; }
void set_format(const std::string &format) { format_ = format; }
void update() override {
optional<time::ESPTime> res;
if (this->sunrise_)
res = this->parent_->sunrise(this->elevation_);
else
res = this->parent_->sunset(this->elevation_);
if (!res) {
this->publish_state("");
return;
}
this->publish_state(res->strftime(this->format_));
}
void dump_config() override;
protected:
std::string format_{};
Sun *parent_;
double elevation_;
bool sunrise_;
};
} // namespace sun
} // namespace esphome

View file

@ -10,7 +10,7 @@ TemplateBinarySensor = template_ns.class_('TemplateBinarySensor', binary_sensor.
CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(TemplateBinarySensor),
cv.Optional(CONF_LAMBDA): cv.lambda_,
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
}).extend(cv.COMPONENT_SCHEMA)

View file

@ -18,7 +18,7 @@ RESTORE_MODES = {
CONFIG_SCHEMA = cover.COVER_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(TemplateCover),
cv.Optional(CONF_LAMBDA): cv.lambda_,
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean,
cv.Optional(CONF_OPEN_ACTION): automation.validate_automation(single=True),

View file

@ -9,7 +9,7 @@ TemplateSensor = template_ns.class_('TemplateSensor', sensor.Sensor, cg.PollingC
CONFIG_SCHEMA = sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 1).extend({
cv.GenerateID(): cv.declare_id(TemplateSensor),
cv.Optional(CONF_LAMBDA): cv.lambda_,
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
}).extend(cv.polling_component_schema('60s'))

View file

@ -10,7 +10,7 @@ TemplateSwitch = template_ns.class_('TemplateSwitch', switch.Switch, cg.Componen
CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(TemplateSwitch),
cv.Optional(CONF_LAMBDA): cv.lambda_,
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean,
cv.Optional(CONF_TURN_OFF_ACTION): automation.validate_automation(single=True),

View file

@ -11,7 +11,7 @@ TemplateTextSensor = template_ns.class_('TemplateTextSensor', text_sensor.TextSe
CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(TemplateTextSensor),
cv.Optional(CONF_LAMBDA): cv.lambda_,
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
}).extend(cv.polling_component_schema('60s'))

View file

@ -4,7 +4,7 @@
namespace esphome {
namespace time {
static const char *TAG = "something.something";
static const char *TAG = "automation";
void CronTrigger::add_second(uint8_t second) { this->seconds_[second] = true; }
void CronTrigger::add_minute(uint8_t minute) { this->minutes_[minute] = true; }
@ -38,11 +38,11 @@ void CronTrigger::loop() {
}
this->last_check_ = time;
if (!time.in_range()) {
if (!time.fields_in_range()) {
ESP_LOGW(TAG, "Time is out of range!");
ESP_LOGD(TAG, "Second=%02u Minute=%02u Hour=%02u DayOfWeek=%u DayOfMonth=%u DayOfYear=%u Month=%u time=%ld",
time.second, time.minute, time.hour, time.day_of_week, time.day_of_month, time.day_of_year, time.month,
time.time);
time.timestamp);
}
if (this->matches(time))

View file

@ -35,27 +35,30 @@ size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) {
return ::strftime(buffer, buffer_len, format, &c_tm);
}
ESPTime ESPTime::from_c_tm(struct tm *c_tm, time_t c_time) {
return ESPTime{.second = uint8_t(c_tm->tm_sec),
.minute = uint8_t(c_tm->tm_min),
.hour = uint8_t(c_tm->tm_hour),
.day_of_week = uint8_t(c_tm->tm_wday + 1),
.day_of_month = uint8_t(c_tm->tm_mday),
.day_of_year = uint16_t(c_tm->tm_yday + 1),
.month = uint8_t(c_tm->tm_mon + 1),
.year = uint16_t(c_tm->tm_year + 1900),
.is_dst = bool(c_tm->tm_isdst),
.time = c_time};
ESPTime res{};
res.second = uint8_t(c_tm->tm_sec);
res.minute = uint8_t(c_tm->tm_min);
res.hour = uint8_t(c_tm->tm_hour);
res.day_of_week = uint8_t(c_tm->tm_wday + 1);
res.day_of_month = uint8_t(c_tm->tm_mday);
res.day_of_year = uint16_t(c_tm->tm_yday + 1);
res.month = uint8_t(c_tm->tm_mon + 1);
res.year = uint16_t(c_tm->tm_year + 1900);
res.is_dst = bool(c_tm->tm_isdst);
res.timestamp = c_time;
return res;
}
struct tm ESPTime::to_c_tm() {
struct tm c_tm = tm{.tm_sec = this->second,
.tm_min = this->minute,
.tm_hour = this->hour,
.tm_mday = this->day_of_month,
.tm_mon = this->month - 1,
.tm_year = this->year - 1900,
.tm_wday = this->day_of_week - 1,
.tm_yday = this->day_of_year - 1,
.tm_isdst = this->is_dst};
struct tm c_tm {};
c_tm.tm_sec = this->second;
c_tm.tm_min = this->minute;
c_tm.tm_hour = this->hour;
c_tm.tm_mday = this->day_of_month;
c_tm.tm_mon = this->month - 1;
c_tm.tm_year = this->year - 1900;
c_tm.tm_wday = this->day_of_week - 1;
c_tm.tm_yday = this->day_of_year - 1;
c_tm.tm_isdst = this->is_dst;
return c_tm;
}
std::string ESPTime::strftime(const std::string &format) {
@ -70,7 +73,6 @@ std::string ESPTime::strftime(const std::string &format) {
timestr.resize(len);
return timestr;
}
bool ESPTime::is_valid() const { return this->year >= 2018; }
template<typename T> bool increment_time_value(T &current, uint16_t begin, uint16_t end) {
current++;
@ -81,8 +83,18 @@ template<typename T> bool increment_time_value(T &current, uint16_t begin, uint1
return false;
}
static bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); }
static bool days_in_month(uint8_t month, uint16_t year) {
static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
uint8_t days_in_month = DAYS_IN_MONTH[month];
if (month == 2 && is_leap_year(year))
days_in_month = 29;
return days_in_month;
}
void ESPTime::increment_second() {
this->time++;
this->timestamp++;
if (!increment_time_value(this->second, 0, 60))
return;
@ -97,12 +109,7 @@ void ESPTime::increment_second() {
// hour roll-over, increment day
increment_time_value(this->day_of_week, 1, 8);
static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
uint8_t days_in_month = DAYS_IN_MONTH[this->month];
if (this->month == 2 && this->year % 4 == 0)
days_in_month = 29;
if (increment_time_value(this->day_of_month, 1, days_in_month + 1)) {
if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) {
// day of month roll-over, increment month
increment_time_value(this->month, 1, 13);
}
@ -113,16 +120,39 @@ void ESPTime::increment_second() {
this->year++;
}
}
bool ESPTime::operator<(ESPTime other) { return this->time < other.time; }
bool ESPTime::operator<=(ESPTime other) { return this->time <= other.time; }
bool ESPTime::operator==(ESPTime other) { return this->time == other.time; }
bool ESPTime::operator>=(ESPTime other) { return this->time >= other.time; }
bool ESPTime::operator>(ESPTime other) { return this->time > other.time; }
bool ESPTime::in_range() const {
return this->second < 61 && this->minute < 60 && this->hour < 24 && this->day_of_week > 0 && this->day_of_week < 8 &&
this->day_of_month > 0 && this->day_of_month < 32 && this->day_of_year > 0 && this->day_of_year < 367 &&
this->month > 0 && this->month < 13;
void ESPTime::recalc_timestamp_utc(bool use_day_of_year) {
time_t res = 0;
if (!this->fields_in_range()) {
this->timestamp = -1;
return;
}
for (uint16_t i = 1970; i < this->year; i++)
res += is_leap_year(i) ? 366 : 365;
if (use_day_of_year) {
res += this->day_of_year - 1;
} else {
for (uint8_t i = 1; i < this->month; ++i)
res += days_in_month(i, this->year);
res += this->day_of_month - 1;
}
res *= 24;
res += this->hour;
res *= 60;
res += this->minute;
res *= 60;
res += this->second;
this->timestamp = res;
}
bool ESPTime::operator<(ESPTime other) { return this->timestamp < other.timestamp; }
bool ESPTime::operator<=(ESPTime other) { return this->timestamp <= other.timestamp; }
bool ESPTime::operator==(ESPTime other) { return this->timestamp == other.timestamp; }
bool ESPTime::operator>=(ESPTime other) { return this->timestamp >= other.timestamp; }
bool ESPTime::operator>(ESPTime other) { return this->timestamp > other.timestamp; }
} // namespace time
} // namespace esphome

View file

@ -1,6 +1,7 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include <stdlib.h>
#include <time.h>
#include <bitset>
@ -30,8 +31,11 @@ struct ESPTime {
uint16_t year;
/// daylight savings time flag
bool is_dst;
union {
ESPDEPRECATED(".time is deprecated, use .timestamp instead") time_t time;
/// unix epoch time (seconds since UTC Midnight January 1, 1970)
time_t time;
time_t timestamp;
};
/** Convert this ESPTime struct to a null-terminated c string buffer as specified by the format argument.
* Up to buffer_len bytes are written.
@ -48,13 +52,20 @@ struct ESPTime {
*/
std::string strftime(const std::string &format);
bool is_valid() const;
/// Check if this ESPTime is valid (all fields in range and year is greater than 2018)
bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); }
bool in_range() const;
/// Check if all time fields of this ESPTime are in range.
bool fields_in_range() const {
return this->second < 61 && this->minute < 60 && this->hour < 24 && this->day_of_week > 0 &&
this->day_of_week < 8 && this->day_of_month > 0 && this->day_of_month < 32 && this->day_of_year > 0 &&
this->day_of_year < 367 && this->month > 0 && this->month < 13;
}
/// Convert a C tm struct instance with a C unix epoch timestamp to an ESPTime instance.
static ESPTime from_c_tm(struct tm *c_tm, time_t c_time);
/** Convert an epoch timestamp to an ESPTime instance of local time.
/** Convert an UTC epoch timestamp to a local time ESPTime instance.
*
* @param epoch Seconds since 1st January 1970. In UTC.
* @return The generated ESPTime
@ -63,7 +74,7 @@ struct ESPTime {
struct tm *c_tm = ::localtime(&epoch);
return ESPTime::from_c_tm(c_tm, epoch);
}
/** Convert an epoch timestamp to an ESPTime instance of UTC time.
/** Convert an UTC epoch timestamp to a UTC time ESPTime instance.
*
* @param epoch Seconds since 1st January 1970. In UTC.
* @return The generated ESPTime
@ -73,8 +84,13 @@ struct ESPTime {
return ESPTime::from_c_tm(c_tm, epoch);
}
/// Recalculate the timestamp field from the other fields of this ESPTime instance (must be UTC).
void recalc_timestamp_utc(bool use_day_of_year = true);
/// Convert this ESPTime instance back to a tm struct.
struct tm to_c_tm();
/// Increment this clock instance by one second.
void increment_second();
bool operator<(ESPTime other);
bool operator<=(ESPTime other);
@ -100,10 +116,10 @@ class RealTimeClock : public Component {
std::string get_timezone() { return this->timezone_; }
/// Get the time in the currently defined timezone.
ESPTime now() { return ESPTime::from_epoch_utc(this->timestamp_now()); }
ESPTime now() { return ESPTime::from_epoch_local(this->timestamp_now()); }
/// Get the time without any time zone or DST corrections.
ESPTime utcnow() { return ESPTime::from_epoch_local(this->timestamp_now()); }
ESPTime utcnow() { return ESPTime::from_epoch_utc(this->timestamp_now()); }
/// Get the current time as the UTC epoch since January 1st 1970.
time_t timestamp_now() { return ::time(nullptr); }

View file

@ -10,7 +10,7 @@ TTP229Channel = ttp229_lsf_ns.class_('TTP229Channel', binary_sensor.BinarySensor
CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(TTP229Channel),
cv.GenerateID(CONF_TTP229_ID): cv.use_id(TTP229LSFComponent),
cv.Required(CONF_CHANNEL): cv.All(cv.int_, cv.Range(min=0, max=15))
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=15),
})

View file

@ -19,7 +19,7 @@ def validate_rx_pin(value):
CONFIG_SCHEMA = cv.All(cv.Schema({
cv.GenerateID(): cv.declare_id(UARTComponent),
cv.Required(CONF_BAUD_RATE): cv.All(cv.int_, cv.Range(min=1, max=115200)),
cv.Required(CONF_BAUD_RATE): cv.int_range(min=1, max=115200),
cv.Optional(CONF_TX_PIN): pins.output_pin,
cv.Optional(CONF_RX_PIN): validate_rx_pin,
}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN))

View file

@ -7,6 +7,7 @@ import re
import os.path
# pylint: disable=unused-import, wrong-import-order
import sys
from contextlib import contextmanager
import voluptuous as vol
@ -17,7 +18,7 @@ from esphome.components.substitutions import CONF_SUBSTITUTIONS
from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS
from esphome.core import CORE, EsphomeError # noqa
from esphome.helpers import color, indent
from esphome.py_compat import text_type
from esphome.py_compat import text_type, IS_PY2
from esphome.util import safe_print, OrderedDict
from typing import List, Optional, Tuple, Union # noqa
@ -69,10 +70,6 @@ class ComponentManifest(object):
def auto_load(self):
return getattr(self.module, 'AUTO_LOAD', [])
@property
def to_code_priority(self):
return getattr(self.module, 'TO_CODE_PRIORITY', [])
def _get_flags_set(self, name, config):
if not hasattr(self.module, name):
return set()
@ -110,24 +107,59 @@ class ComponentManifest(object):
CORE_COMPONENTS_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), 'components'))
_UNDEF = object()
CUSTOM_COMPONENTS_PATH = _UNDEF
def _mount_config_dir():
global CUSTOM_COMPONENTS_PATH
if CUSTOM_COMPONENTS_PATH is not _UNDEF:
return
custom_path = os.path.abspath(os.path.join(CORE.config_dir, 'custom_components'))
if not os.path.isdir(custom_path):
CUSTOM_COMPONENTS_PATH = None
return
init_path = os.path.join(custom_path, '__init__.py')
if IS_PY2 and not os.path.isfile(init_path):
_LOGGER.warning("Found 'custom_components' folder, but file __init__.py was not found. "
"ESPHome will automatically create it now....")
with open(init_path, 'w') as f:
f.write('\n')
if CORE.config_dir not in sys.path:
sys.path.insert(0, CORE.config_dir)
CUSTOM_COMPONENTS_PATH = custom_path
def _lookup_module(domain, is_platform):
if domain in _COMPONENT_CACHE:
return _COMPONENT_CACHE[domain]
path = 'esphome.components.{}'.format(domain)
_mount_config_dir()
# First look for custom_components
try:
module = importlib.import_module(path)
except ImportError:
import traceback
_LOGGER.error("Unable to import component %s:", domain)
traceback.print_exc()
module = importlib.import_module('custom_components.{}'.format(domain))
except ImportError as e:
# ImportError when no such module
if 'No module named' not in str(e):
_LOGGER.warn("Unable to import custom component %s:", domain, exc_info=True)
except Exception: # pylint: disable=broad-except
# Other error means component has an issue
_LOGGER.error("Unable to load custom component %s:", domain, exc_info=True)
return None
else:
# Found in custom components
manif = ComponentManifest(module, CUSTOM_COMPONENTS_PATH, is_platform=is_platform)
_COMPONENT_CACHE[domain] = manif
return manif
try:
module = importlib.import_module('esphome.components.{}'.format(domain))
except ImportError as e:
if 'No module named' not in str(e):
_LOGGER.error("Unable to import component %s:", domain, exc_info=True)
return None
except Exception: # pylint: disable=broad-except
import traceback
_LOGGER.error("Unable to load component %s:", domain)
traceback.print_exc()
_LOGGER.error("Unable to load component %s:", domain, exc_info=True)
return None
else:
manif = ComponentManifest(module, CORE_COMPONENTS_PATH, is_platform=is_platform)
@ -233,15 +265,19 @@ class Config(OrderedDict):
return err
return None
def get_deepest_value_for_path(self, path):
# type: (ConfigPath) -> ConfigType
def get_deepest_document_range_for_path(self, path):
# type: (ConfigPath) -> Optional[ESPHomeDataBase]
data = self
doc_range = None
for item_index in path:
try:
data = data[item_index]
except (KeyError, IndexError, TypeError):
return data
return data
return doc_range
if isinstance(data, ESPHomeDataBase) and data.esp_range is not None:
doc_range = data.esp_range
return doc_range
def get_nested_item(self, path):
# type: (ConfigPath) -> ConfigType

View file

@ -2,6 +2,7 @@ from __future__ import print_function
import codecs
import json
import os
from esphome.core import CORE, EsphomeError
from esphome.py_compat import safe_input
@ -9,7 +10,8 @@ from esphome.py_compat import safe_input
def read_config_file(path):
# type: (basestring) -> unicode
if CORE.vscode:
if CORE.vscode and (not CORE.ace or
os.path.abspath(path) == os.path.abspath(CORE.config_path)):
print(json.dumps({
'type': 'read_file',
'path': path,

View file

@ -8,6 +8,7 @@ import re
from contextlib import contextmanager
import uuid as uuid_
from datetime import datetime
from string import ascii_letters, digits
import voluptuous as vol
@ -279,8 +280,9 @@ def validate_id_name(value):
raise Invalid("First character in ID cannot be a digit.")
if '-' in value:
raise Invalid("Dashes are not supported in IDs, please use underscores instead.")
valid_chars = ascii_letters + digits + '_'
for char in value:
if char != '_' and not char.isalnum():
if char not in valid_chars:
raise Invalid(u"IDs must only consist of upper/lowercase characters, the underscore"
u"character and numbers. The character '{}' cannot be used"
u"".format(char))
@ -333,7 +335,7 @@ def templatable(other_validators):
def validator(value):
if isinstance(value, Lambda):
return lambda_(value)
return returning_lambda(value)
if isinstance(other_validators, dict):
return schema(value)
return schema(value)
@ -570,10 +572,15 @@ METRIC_SUFFIXES = {
}
def float_with_unit(quantity, regex_suffix):
def float_with_unit(quantity, regex_suffix, optional_unit=False):
pattern = re.compile(r"^([-+]?[0-9]*\.?[0-9]*)\s*(\w*?)" + regex_suffix + r"$", re.UNICODE)
def validator(value):
if optional_unit:
try:
return float_(value)
except Invalid:
pass
match = pattern.match(string(value))
if match is None:
@ -595,6 +602,7 @@ current = float_with_unit("current", u"(a|A|amp|Amp|amps|Amps|ampere|Ampere)?")
voltage = float_with_unit("voltage", u"(v|V|volt|Volts)?")
distance = float_with_unit("distance", u"(m)")
framerate = float_with_unit("framerate", u"(FPS|fps|Fps|FpS|Hz)")
angle = float_with_unit("angle", u"(°|deg)", optional_unit=True)
_temperature_c = float_with_unit("temperature", u"(°C|° C|°|C)?")
_temperature_k = float_with_unit("temperature", u"(° K|° K|K)?")
_temperature_f = float_with_unit("temperature", u"(°F|° F|F)?")
@ -605,7 +613,7 @@ if IS_PY2:
path = u' @ data[%s]' % u']['.join(map(repr, self.path)) \
if self.path else ''
# pylint: disable=no-member
output = Exception.__unicode__(self)
output = self.message
if self.error_type:
output += u' for ' + self.error_type
return output + path
@ -974,6 +982,20 @@ def lambda_(value):
return value
def returning_lambda(value):
"""Coerce this configuration option to a lambda.
Additionally, make sure the lambda returns something.
"""
value = lambda_(value)
if u'return' not in value.value:
raise Invalid("Lambda doesn't contain a 'return' statement, but the lambda "
"is expected to return a value. \n"
"Please make sure the lambda contains at least one "
"return statement.")
return value
def dimensions(value):
if isinstance(value, list):
if len(value) != 2:

View file

@ -11,8 +11,6 @@ ESP_PLATFORM_ESP32 = 'ESP32'
ESP_PLATFORM_ESP8266 = 'ESP8266'
ESP_PLATFORMS = [ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266]
APB_CLOCK_FREQ = 80000000
ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_'
ARDUINO_VERSION_ESP32_DEV = 'https://github.com/platformio/platform-espressif32.git#feature/stage'
ARDUINO_VERSION_ESP32_1_0_0 = 'espressif32@1.5.0'
@ -472,6 +470,9 @@ ICON_ROTATE_RIGHT = 'mdi:rotate-right'
ICON_SCALE = 'mdi:scale'
ICON_SCREEN_ROTATION = 'mdi:screen-rotation'
ICON_SIGNAL = 'mdi:signal'
ICON_WEATHER_SUNSET = 'mdi:weather-sunset'
ICON_WEATHER_SUNSET_DOWN = 'mdi:weather-sunset-down'
ICON_WEATHER_SUNSET_UP = 'mdi:weather-sunset-up'
ICON_THERMOMETER = 'mdi:thermometer'
ICON_TIMER = 'mdi:timer'
ICON_WATER_PERCENT = 'mdi:water-percent'

View file

@ -467,6 +467,7 @@ class EsphomeCore(object):
self.dashboard = False
# True if command is run from vscode api
self.vscode = False
self.ace = False
# The name of the node
self.name = None # type: str
# The relative path to the configuration YAML

View file

@ -60,7 +60,7 @@ void Application::setup() {
this->dump_config();
}
void Application::dump_config() {
ESP_LOGI(TAG, "esphome-core version " ESPHOME_VERSION " compiled on %s", this->compilation_time_.c_str());
ESP_LOGI(TAG, "esphome version " ESPHOME_VERSION " compiled on %s", this->compilation_time_.c_str());
for (auto component : this->components_) {
component->dump_config();

View file

@ -294,8 +294,6 @@ void HighFrequencyLoopRequester::stop() {
bool HighFrequencyLoopRequester::is_high_frequency() { return high_freq_num_requests > 0; }
float clamp(float val, float min, float max) {
if (min > max)
std::swap(min, max);
if (val < min)
return min;
if (val > max)

View file

@ -254,6 +254,18 @@ template<typename T> class Deduplicator {
T last_value_{};
};
template<typename T> class Parented {
public:
Parented() {}
Parented(T *parent) : parent_(parent) {}
T *get_parent() const { return parent_; }
void set_parent(T *parent) { parent_ = parent; }
protected:
T *parent_{nullptr};
};
uint32_t fnv1_hash(const std::string &str);
} // namespace esphome

View file

@ -89,8 +89,7 @@ def default_build_path():
CONFIG_SCHEMA = cv.Schema({
cv.Required(CONF_NAME): cv.valid_name,
cv.Required(CONF_PLATFORM): cv.one_of('ESP8266', 'ESP32', 'ESPRESSIF32',
upper=True),
cv.Required(CONF_PLATFORM): cv.one_of('ESP8266', 'ESP32', upper=True),
cv.Required(CONF_BOARD): validate_board,
cv.Optional(CONF_ARDUINO_VERSION, default='recommended'): validate_arduino_version,
cv.Optional(CONF_BUILD_PATH, default=default_build_path): cv.string,
@ -114,6 +113,10 @@ CONFIG_SCHEMA = cv.Schema({
}),
cv.Optional(CONF_INCLUDES, default=[]): cv.ensure_list(cv.file_),
cv.Optional(CONF_LIBRARIES, default=[]): cv.ensure_list(cv.string_strict),
cv.Optional('esphome_core_version'): cv.invalid("The esphome_core_version option has been "
"removed in 1.13 - the esphome core source "
"files are now bundled with ESPHome.")
})
PRELOAD_CONFIG_SCHEMA = cv.Schema({

View file

@ -328,6 +328,17 @@ class ExpressionStatement(Statement):
return u"{};".format(self.expression)
class LineComment(Statement):
def __init__(self, value): # type: (unicode) -> None
super(LineComment, self).__init__()
self._value = value
def __str__(self):
parts = self._value.split(u'\n')
parts = [u'// {}'.format(x) for x in parts]
return u'\n'.join(parts)
class ProgmemAssignmentExpression(AssignmentExpression):
def __init__(self, type, name, rhs, obj):
super(ProgmemAssignmentExpression, self).__init__(
@ -424,10 +435,14 @@ def new_Pvariable(id, # type: ID
return Pvariable(id, rhs)
def add(expression, # type: Union[SafeExpType, Statement]
def add(expression, # type: Union[Expression, Statement]
):
# type: (...) -> None
"""Add an expression to the codegen setup() storage."""
"""Add an expression to the codegen section.
After this is called, the given given expression will
show up in the setup() function after this has been called.
"""
CORE.add(expression)

View file

@ -1,7 +1,7 @@
from esphome.const import CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_SETUP_PRIORITY, \
CONF_UPDATE_INTERVAL, CONF_TYPE_ID
from esphome.core import coroutine
from esphome.cpp_generator import RawExpression, add
from esphome.core import coroutine, ID
from esphome.cpp_generator import RawExpression, add, get_variable
from esphome.cpp_types import App, GPIOPin
@ -42,6 +42,15 @@ def register_component(var, config):
yield var
@coroutine
def register_parented(var, value):
if isinstance(value, ID):
paren = yield get_variable(value)
else:
paren = value
add(var.set_parent(paren))
def extract_registry_entry_config(registry, full_config):
# type: (Registry, ConfigType) -> RegistryEntry
key, config = next((k, v) for k, v in full_config.items() if k in registry)

View file

@ -4,6 +4,7 @@ global_ns = MockObj('', '')
void = global_ns.namespace('void')
nullptr = global_ns.namespace('nullptr')
float_ = global_ns.namespace('float')
double = global_ns.namespace('double')
bool_ = global_ns.namespace('bool')
std_ns = global_ns.namespace('std')
std_string = std_ns.class_('string')

View file

@ -256,12 +256,12 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
self.write_message({'event': 'exit', 'code': returncode})
def on_close(self):
# Shutdown proc on WS close
self._is_closed = True
# Check if proc exists (if 'start' has been run)
if self.is_process_active:
_LOGGER.debug("Terminating process")
self._proc.proc.terminate()
# Shutdown proc on WS close
self._is_closed = True
def build_command(self, json_message):
raise NotImplementedError
@ -310,6 +310,11 @@ class EsphomeVscodeHandler(EsphomeCommandWebSocket):
return ["esphome", "--dashboard", "-q", 'dummy', "vscode"]
class EsphomeAceEditorHandler(EsphomeCommandWebSocket):
def build_command(self, json_message):
return ["esphome", "--dashboard", "-q", settings.config_dir, "vscode", "--ace"]
class SerialPortRequestHandler(BaseHandler):
@authenticated
def get(self):
@ -678,6 +683,7 @@ def make_app(debug=False):
(rel + "clean-mqtt", EsphomeCleanMqttHandler),
(rel + "clean", EsphomeCleanHandler),
(rel + "vscode", EsphomeVscodeHandler),
(rel + "ace", EsphomeAceEditorHandler),
(rel + "edit", EditRequestHandler),
(rel + "download.bin", DownloadBinaryRequestHandler),
(rel + "serial-ports", SerialPortRequestHandler),

View file

@ -14,4 +14,3 @@
}
});
})();

View file

@ -550,10 +550,75 @@ const editModalElem = document.getElementById("modal-editor");
const editorElem = editModalElem.querySelector("#editor");
const editor = ace.edit(editorElem);
let activeEditorConfig = null;
let aceWs = null;
let aceValidationScheduled = false;
let aceValidationRunning = false;
const startAceWebsocket = () => {
aceWs = new WebSocket(`${wsUrl}ace`);
aceWs.addEventListener('message', (event) => {
const raw = JSON.parse(event.data);
if (raw.event === "line") {
const msg = JSON.parse(raw.data);
if (msg.type === "result") {
console.log(msg);
const arr = [];
for (const v of msg.validation_errors) {
let o = {
text: v.message,
type: 'error',
row: 0,
column: 0
};
if (v.range != null) {
o.row = v.range.start_line;
o.column = v.range.start_col;
}
arr.push(o);
}
for (const v of msg.yaml_errors) {
arr.push({
text: v.message,
type: 'error',
row: 0,
column: 0
});
}
editor.session.setAnnotations(arr);
aceValidationRunning = false;
} else if (msg.type === "read_file") {
sendAceStdin({
type: 'file_response',
content: editor.getValue()
});
}
}
});
aceWs.addEventListener('open', () => {
const msg = JSON.stringify({type: 'spawn'});
aceWs.send(msg);
});
aceWs.addEventListener('close', () => {
aceWs = null;
setTimeout(startAceWebsocket, 5000)
});
};
const sendAceStdin = (data) => {
let send = JSON.stringify({
type: 'stdin',
data: JSON.stringify(data)+'\n',
});
aceWs.send(send);
};
startAceWebsocket();
editor.setTheme("ace/theme/dreamweaver");
editor.session.setMode("ace/mode/yaml");
editor.session.setOption('useSoftTabs', true);
editor.session.setOption('tabSize', 2);
editor.session.setOption('useWorker', false);
const saveButton = editModalElem.querySelector(".save-button");
const saveValidateButton = editModalElem.querySelector(".save-validate-button");
@ -569,6 +634,19 @@ const saveEditor = () => {
});
};
const debounce = (func, wait) => {
let timeout;
return function() {
let context = this, args = arguments;
let later = function() {
timeout = null;
func.apply(context, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
editor.commands.addCommand({
name: 'saveCommand',
bindKey: {win: 'Ctrl-S', mac: 'Command-S'},
@ -576,6 +654,24 @@ editor.commands.addCommand({
readOnly: false
});
editor.session.on('change', debounce(() => {
aceValidationScheduled = true;
}, 250));
setInterval(() => {
if (!aceValidationScheduled || aceValidationRunning)
return;
if (aceWs == null)
return;
sendAceStdin({
type: 'validate',
file: activeEditorConfig
});
aceValidationRunning = true;
aceValidationScheduled = false;
}, 100);
saveButton.addEventListener('click', saveEditor);
saveValidateButton.addEventListener('click', saveEditor);
@ -612,3 +708,11 @@ const startWizard = () => {
};
setupWizardStart.addEventListener('click', startWizard);
jQuery.validator.addMethod("nospaces", (value, element) => {
return value.indexOf(' ') < 0;
}, "Name must not contain spaces.");
jQuery.validator.addMethod("lowercase", (value, element) => {
return value === value.toLowerCase();
}, "Name must be lowercase.");

View file

@ -5,4 +5,3 @@ ace.define("ace/ext/searchbox",["require","exports","module","ace/lib/dom","ace/
}
});
})();

0
esphome/dashboard/static/materialize-stepper.min.css vendored Executable file → Normal file
View file

0
esphome/dashboard/static/materialize-stepper.min.js vendored Executable file → Normal file
View file

View file

@ -5,4 +5,3 @@ ace.define("ace/mode/yaml_highlight_rules",["require","exports","module","ace/li
}
});
})();

View file

@ -5,4 +5,3 @@ ace.define("ace/theme/dreamweaver",["require","exports","module","ace/lib/dom"],
}
});
})();

View file

@ -189,7 +189,7 @@
<code class="inlinecode">0-9</code> and <code class="inlinecode">_</code>)
</p>
<div class="input-field col s12">
<input id="node_name" class="validate" type="text" name="name" required>
<input id="node_name" class="validate" type="text" name="name" data-rule-nospaces="true" data-rule-lowercase="true" required>
<label for="node_name">Name of node</label>
</div>
</div>
@ -207,8 +207,7 @@
Please choose the board you're using below.
</p>
<p>
If you're not sure you can also use similar ones or even the
"Generic" option. In most cases that will work too.
<em>If unsure you can also select a similar board or choose the "Generic" option.</em>
</p>
<div class="input-field col s12">
<select id="board" name="board" required>

0
esphome/espota2.py Executable file → Normal file
View file

View file

@ -1,19 +1,16 @@
from __future__ import print_function
import json
import os
from esphome.config import load_config, _format_vol_invalid
from esphome.core import CORE
from esphome.py_compat import text_type, safe_input
from esphome.yaml_util import ESPHomeDataBase
def _get_invalid_range(res, invalid):
# type: (Config, vol.Invalid) -> Optional[DocumentRange]
obj = res.get_deepest_value_for_path(invalid.path)
if isinstance(obj, ESPHomeDataBase) and obj.esp_range is not None:
return obj.esp_range
return None
return res.get_deepest_document_range_for_path(invalid.path)
def _dump_range(range):
@ -53,12 +50,17 @@ class VSCodeResult(object):
})
def read_config():
def read_config(args):
while True:
CORE.reset()
data = json.loads(safe_input())
assert data['type'] == 'validate'
CORE.vscode = True
CORE.ace = args.ace
f = data['file']
if CORE.ace:
CORE.config_path = os.path.join(args.configuration, f)
else:
CORE.config_path = data['file']
vs = VSCodeResult()
try:
@ -67,6 +69,9 @@ def read_config():
vs.add_yaml_error(text_type(err))
else:
for err in res.errors:
try:
range_ = _get_invalid_range(res, err)
vs.add_validation_error(range_, _format_vol_invalid(err, res))
except Exception: # pylint: disable=broad-except
continue
print(vs.dump())

View file

@ -266,7 +266,7 @@ def write_platformio_project():
write_platformio_ini(content)
DEFINES_H_FORMAT = u"""\
DEFINES_H_FORMAT = ESPHOME_H_FORMAT = u"""\
#pragma once
{}
"""
@ -301,7 +301,7 @@ def copy_src_tree():
source_files_l = [it for it in source_files.items()]
source_files_l.sort()
# Build #include list for main.cpp
# Build #include list for esphome.h
include_l = []
for target, path in source_files_l:
if os.path.splitext(path)[1] in HEADER_FILE_EXTENSIONS:
@ -341,8 +341,8 @@ def copy_src_tree():
CORE.relative_src_path('esphome', 'core', 'defines.h'))
write_file_if_changed(ESPHOME_README_TXT,
CORE.relative_src_path('esphome', 'README.txt'))
return include_s
write_file_if_changed(ESPHOME_H_FORMAT.format(include_s),
CORE.relative_src_path('esphome.h'))
def generate_defines_h():
@ -361,8 +361,8 @@ def write_cpp(code_s):
else:
code_format = CPP_BASE_FORMAT
include_s = copy_src_tree()
global_s = include_s + u'\n'
copy_src_tree()
global_s = u'#include "esphome.h"\n'
global_s += CORE.cpp_global_section
full_file = code_format[0] + CPP_INCLUDE_BEGIN + u'\n' + global_s + CPP_INCLUDE_END

View file

@ -1,93 +1,9 @@
#!/usr/bin/env python
import codecs
import json
import os
import re
import sys
import os.path
root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, '..', '..')))
basepath = os.path.join(root_path, 'esphome')
temp_header_file = os.path.join(root_path, '.temp-clang-tidy.cpp')
def walk_files(path):
for root, _, files in os.walk(path):
for name in files:
yield os.path.join(root, name)
def shlex_quote(s):
if not s:
return u"''"
if re.search(r'[^\w@%+=:,./-]', s) is None:
return s
return u"'" + s.replace(u"'", u"'\"'\"'") + u"'"
def build_all_include():
# Build a cpp file that includes all header files in this repo.
# Otherwise header-only integrations would not be tested by clang-tidy
headers = []
for path in walk_files(basepath):
filetypes = ('.h',)
ext = os.path.splitext(path)[1]
if ext in filetypes:
path = os.path.relpath(path, root_path)
include_p = path.replace(os.path.sep, '/')
headers.append('#include "{}"'.format(include_p))
headers.sort()
headers.append('')
content = '\n'.join(headers)
with codecs.open(temp_header_file, 'w', encoding='utf-8') as f:
f.write(content)
def build_compile_commands():
gcc_flags_json = os.path.join(root_path, '.gcc-flags.json')
if not os.path.isfile(gcc_flags_json):
print("Could not find {} file which is required for clang-tidy.")
print('Please run "pio init --ide atom" in the root esphome folder to generate that file.')
sys.exit(1)
with codecs.open(gcc_flags_json, 'r', encoding='utf-8') as f:
gcc_flags = json.load(f)
exec_path = gcc_flags['execPath']
include_paths = gcc_flags['gccIncludePaths'].split(',')
includes = ['-I{}'.format(p) for p in include_paths]
cpp_flags = gcc_flags['gccDefaultCppFlags'].split(' ')
defines = [flag for flag in cpp_flags if flag.startswith('-D')]
command = [exec_path]
command.extend(includes)
command.extend(defines)
command.append('-std=gnu++11')
command.append('-Wall')
command.append('-Wno-delete-non-virtual-dtor')
command.append('-Wno-unused-variable')
command.append('-Wunreachable-code')
source_files = []
for path in walk_files(basepath):
filetypes = ('.cpp',)
ext = os.path.splitext(path)[1]
if ext in filetypes:
source_files.append(os.path.abspath(path))
source_files.append(temp_header_file)
source_files.sort()
compile_commands = [{
'directory': root_path,
'command': ' '.join(shlex_quote(x) for x in (command + ['-o', p + '.o', '-c', p])),
'file': p
} for p in source_files]
compile_commands_json = os.path.join(root_path, 'compile_commands.json')
if os.path.isfile(compile_commands_json):
with codecs.open(compile_commands_json, 'r', encoding='utf-8') as f:
try:
if json.load(f) == compile_commands:
return
except:
pass
with codecs.open(compile_commands_json, 'w', encoding='utf-8') as f:
json.dump(compile_commands, f, indent=2)
sys.path.append(os.path.dirname(__file__))
from helpers import build_all_include, build_compile_commands
def main():

View file

@ -3,7 +3,9 @@ from __future__ import print_function
import codecs
import collections
import fnmatch
import os.path
import subprocess
import sys
@ -18,43 +20,191 @@ def find_all(a_str, sub):
column += len(sub)
files = []
for root, _, fs in os.walk('esphome'):
for f in fs:
_, ext = os.path.splitext(f)
if ext in ('.h', '.c', '.cpp', '.tcc', '.yaml', '.yml', '.ini', '.txt',
'.py', '.html', '.js', '.md'):
files.append(os.path.join(root, f))
ignore = [
'esphome/dashboard/static/materialize.min.js',
'esphome/dashboard/static/ace.js',
'esphome/dashboard/static/mode-yaml.js',
'esphome/dashboard/static/theme-dreamweaver.js',
'esphome/dashboard/static/jquery.validate.min.js',
'esphome/dashboard/static/ext-searchbox.js',
]
files = [f for f in files if f not in ignore]
command = ['git', 'ls-files', '-s']
proc = subprocess.Popen(command, stdout=subprocess.PIPE)
output, err = proc.communicate()
lines = [x.split() for x in output.decode('utf-8').splitlines()]
EXECUTABLE_BIT = {
s[3].strip(): int(s[0]) for s in lines
}
files = [s[3].strip() for s in lines]
files.sort()
file_types = ('.h', '.c', '.cpp', '.tcc', '.yaml', '.yml', '.ini', '.txt', '.ico',
'.py', '.html', '.js', '.md', '.sh', '.css', '.proto', '.conf', '.cfg')
cpp_include = ('*.h', '*.c', '*.cpp', '*.tcc')
ignore_types = ('.ico',)
LINT_FILE_CHECKS = []
LINT_CONTENT_CHECKS = []
def run_check(lint_obj, fname, *args):
include = lint_obj['include']
exclude = lint_obj['exclude']
func = lint_obj['func']
if include is not None:
for incl in include:
if fnmatch.fnmatch(fname, incl):
break
else:
return None
for excl in exclude:
if fnmatch.fnmatch(fname, excl):
return None
return func(*args)
def run_checks(lints, fname, *args):
for lint in lints:
add_errors(fname, run_check(lint, fname, *args))
def _add_check(checks, func, include=None, exclude=None):
checks.append({
'include': include,
'exclude': exclude or [],
'func': func,
})
def lint_file_check(**kwargs):
def decorator(func):
_add_check(LINT_FILE_CHECKS, func, **kwargs)
return func
return decorator
def lint_content_check(**kwargs):
def decorator(func):
_add_check(LINT_CONTENT_CHECKS, func, **kwargs)
return func
return decorator
def lint_content_find_check(find, **kwargs):
decor = lint_content_check(**kwargs)
def decorator(func):
def new_func(content):
for line, col in find_all(content, find):
err = func()
return "{err} See line {line}:{col}.".format(err=err, line=line+1, col=col+1)
return decor(new_func)
return decorator
@lint_file_check(include=['*.ino'])
def lint_ino(fname):
return "This file extension (.ino) is not allowed. Please use either .cpp or .h"
@lint_file_check(exclude=['*{}'.format(f) for f in file_types] + [
'.clang-*', '.dockerignore', '.editorconfig', '*.gitignore', 'LICENSE', 'pylintrc',
'MANIFEST.in', 'docker/Dockerfile*', 'docker/rootfs/*', 'script/*'
])
def lint_ext_check(fname):
return "This file extension is not a registered file type. If this is an error, please " \
"update the script/ci-custom.py script."
@lint_file_check(exclude=[
'docker/rootfs/*', 'script/*', 'setup.py'
])
def lint_executable_bit(fname):
ex = EXECUTABLE_BIT[fname]
if ex != 100644:
return 'File has invalid executable bit {}. If running from a windows machine please ' \
'see disabling executable bit in git.'.format(ex)
return None
@lint_content_find_check('\t', exclude=[
'esphome/dashboard/static/ace.js', 'esphome/dashboard/static/ext-searchbox.js',
'script/.neopixelbus.patch',
])
def lint_tabs():
return "File contains tab character. Please convert tabs to spaces."
@lint_content_find_check('\r')
def lint_newline():
return "File contains windows newline. Please set your editor to unix newline mode."
@lint_content_check()
def lint_end_newline(content):
if content and not content.endswith('\n'):
return "File does not end with a newline, please add an empty line at the end of the file."
return None
@lint_content_find_check('"esphome.h"', include=cpp_include, exclude=['tests/custom.h'])
def lint_esphome_h():
return ("File contains reference to 'esphome.h' - This file is "
"auto-generated and should only be used for *custom* "
"components. Please replace with references to the direct files.")
@lint_content_check(include=['*.h'])
def lint_pragma_once(content):
if '#pragma once' not in content:
return ("Header file contains no 'pragma once' header guard. Please add a "
"'#pragma once' line at the top of the file.")
return None
@lint_content_find_check('ESP_LOG', include=['*.h', '*.tcc'], exclude=[
'esphome/components/binary_sensor/binary_sensor.h',
'esphome/components/cover/cover.h',
'esphome/components/display/display_buffer.h',
'esphome/components/i2c/i2c.h',
'esphome/components/mqtt/mqtt_component.h',
'esphome/components/output/binary_output.h',
'esphome/components/output/float_output.h',
'esphome/components/sensor/sensor.h',
'esphome/components/stepper/stepper.h',
'esphome/components/switch/switch.h',
'esphome/components/text_sensor/text_sensor.h',
'esphome/core/component.h',
'esphome/core/esphal.h',
'esphome/core/log.h',
'tests/custom.h',
])
def lint_log_in_header():
return ('Found reference to ESP_LOG in header file. Using ESP_LOG* in header files '
'is currently not possible - please move the definition to a source file (.cpp)')
errors = collections.defaultdict(list)
for f in files:
def add_errors(fname, errs):
if not isinstance(errs, list):
errs = [errs]
errs = [x for x in errs if x is not None]
for err in errs:
if not isinstance(err, str):
raise ValueError("Error is not instance of string!")
if not errs:
return
errors[fname].extend(errs)
for fname in files:
_, ext = os.path.splitext(fname)
run_checks(LINT_FILE_CHECKS, fname, fname)
if ext in ('.ico',):
continue
try:
with codecs.open(f, 'r', encoding='utf-8') as f_handle:
with codecs.open(fname, 'r', encoding='utf-8') as f_handle:
content = f_handle.read()
except UnicodeDecodeError:
errors[f].append("File is not readable as UTF-8. Please set your editor to UTF-8 mode.")
add_errors(fname, "File is not readable as UTF-8. Please set your editor to UTF-8 mode.")
continue
for line, col in find_all(content, '\t'):
errors[f].append("File contains tab character on line {}:{}. "
"Please convert tabs to spaces.".format(line, col))
for line, col in find_all(content, '\r'):
errors[f].append("File contains windows newline on line {}:{}. "
"Please set your editor to unix newline mode.".format(line, col))
if content and not content.endswith('\n'):
errors[f].append("File does not end with a newline, please add an empty line at the end of "
"the file.")
run_checks(LINT_CONTENT_CHECKS, fname, content)
for f, errs in errors.items():
for f, errs in sorted(errors.items()):
print("\033[0;32m************* File \033[1;32m{}\033[0m".format(f))
for err in errs:
print(err)

View file

@ -2,16 +2,19 @@
from __future__ import print_function
import argparse
import multiprocessing
import os
import re
import subprocess
import sys
import argparse
import click
import threading
import click
sys.path.append(os.path.dirname(__file__))
from helpers import basepath, get_output, walk_files, filter_changed
is_py2 = sys.version[0] == '2'
if is_py2:
@ -50,44 +53,6 @@ def progress_bar_show(value):
return value
def walk_files(path):
for root, _, files in os.walk(path):
for name in files:
yield os.path.join(root, name)
def get_output(*args):
proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = proc.communicate()
return output.decode('utf-8')
def splitlines_no_ends(string):
return [s.strip() for s in string.splitlines()]
def filter_changed(files):
for remote in ('upstream', 'origin'):
command = ['git', 'merge-base', '{}/dev'.format(remote), 'HEAD']
try:
merge_base = splitlines_no_ends(get_output(*command))[0]
break
except:
pass
else:
return files
command = ['git', 'diff', merge_base, '--name-only']
changed = splitlines_no_ends(get_output(*command))
changed = {os.path.relpath(f, os.getcwd()) for f in changed}
print("Changed Files:")
files = [p for p in files if p in changed]
for p in files:
print(" {}".format(p))
if not files:
print(" No changed files")
return files
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-j', '--jobs', type=int,

View file

@ -2,8 +2,6 @@
from __future__ import print_function
import codecs
import json
import multiprocessing
import os
import re
@ -18,6 +16,10 @@ import argparse
import click
import threading
sys.path.append(os.path.dirname(__file__))
from helpers import basepath, shlex_quote, get_output, build_compile_commands, \
build_all_include, temp_header_file, walk_files, filter_changed
is_py2 = sys.version[0] == '2'
if is_py2:
@ -25,11 +27,6 @@ if is_py2:
else:
import queue as queue
root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, '..', '..')))
basepath = os.path.join(root_path, 'esphome')
rel_basepath = os.path.relpath(basepath, os.getcwd())
temp_header_file = os.path.join(root_path, '.temp-clang-tidy.cpp')
def run_tidy(args, tmpdir, queue, lock, failed_files):
while True:
@ -67,119 +64,6 @@ def run_tidy(args, tmpdir, queue, lock, failed_files):
def progress_bar_show(value):
if value is None:
return ''
return value
def walk_files(path):
for root, _, files in os.walk(path):
for name in files:
yield os.path.join(root, name)
def get_output(*args):
proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = proc.communicate()
return output.decode('utf-8')
def splitlines_no_ends(string):
return [s.strip() for s in string.splitlines()]
def filter_changed(files):
for remote in ('upstream', 'origin'):
command = ['git', 'merge-base', '{}/dev'.format(remote), 'HEAD']
try:
merge_base = splitlines_no_ends(get_output(*command))[0]
break
except:
pass
else:
return files
command = ['git', 'diff', merge_base, '--name-only']
changed = splitlines_no_ends(get_output(*command))
changed = {os.path.relpath(f, os.getcwd()) for f in changed}
print("Changed Files:")
files = [p for p in files if p in changed]
for p in files:
print(" {}".format(p))
if not files:
print(" No changed files")
return files
def shlex_quote(s):
if not s:
return u"''"
if re.search(r'[^\w@%+=:,./-]', s) is None:
return s
return u"'" + s.replace(u"'", u"'\"'\"'") + u"'"
def build_compile_commands():
gcc_flags_json = os.path.join(root_path, '.gcc-flags.json')
if not os.path.isfile(gcc_flags_json):
print("Could not find {} file which is required for clang-tidy.")
print('Please run "pio init --ide atom" in the root esphome folder to generate that file.')
sys.exit(1)
with codecs.open(gcc_flags_json, 'r', encoding='utf-8') as f:
gcc_flags = json.load(f)
exec_path = gcc_flags['execPath']
include_paths = gcc_flags['gccIncludePaths'].split(',')
includes = ['-I{}'.format(p) for p in include_paths]
cpp_flags = gcc_flags['gccDefaultCppFlags'].split(' ')
defines = [flag for flag in cpp_flags if flag.startswith('-D')]
command = [exec_path]
command.extend(includes)
command.extend(defines)
command.append('-std=gnu++11')
command.append('-Wall')
command.append('-Wno-delete-non-virtual-dtor')
command.append('-Wno-unused-variable')
command.append('-Wunreachable-code')
source_files = []
for path in walk_files(basepath):
filetypes = ('.cpp',)
ext = os.path.splitext(path)[1]
if ext in filetypes:
source_files.append(os.path.abspath(path))
source_files.append(temp_header_file)
source_files.sort()
compile_commands = [{
'directory': root_path,
'command': ' '.join(shlex_quote(x) for x in (command + ['-o', p + '.o', '-c', p])),
'file': p
} for p in source_files]
compile_commands_json = os.path.join(root_path, 'compile_commands.json')
if os.path.isfile(compile_commands_json):
with codecs.open(compile_commands_json, 'r', encoding='utf-8') as f:
try:
if json.load(f) == compile_commands:
return
except:
pass
with codecs.open(compile_commands_json, 'w', encoding='utf-8') as f:
json.dump(compile_commands, f, indent=2)
def build_all_include():
# Build a cpp file that includes all header files in this repo.
# Otherwise header-only integrations would not be tested by clang-tidy
headers = []
for path in walk_files(basepath):
filetypes = ('.h',)
ext = os.path.splitext(path)[1]
if ext in filetypes:
path = os.path.relpath(path, root_path)
include_p = path.replace(os.path.sep, '/')
headers.append('#include "{}"'.format(include_p))
headers.sort()
headers.append('')
content = '\n'.join(headers)
with codecs.open(temp_header_file, 'w', encoding='utf-8') as f:
f.write(content)
def main():
@ -212,6 +96,7 @@ def main():
""")
return 1
build_all_include()
build_compile_commands()
files = []
@ -231,7 +116,6 @@ def main():
files.sort()
if args.all_headers:
build_all_include()
files.insert(0, temp_header_file)
tmpdir = None

128
script/helpers.py Normal file
View file

@ -0,0 +1,128 @@
import codecs
import json
import os.path
import re
import subprocess
import sys
root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, '..', '..')))
basepath = os.path.join(root_path, 'esphome')
temp_header_file = os.path.join(root_path, '.temp-clang-tidy.cpp')
def shlex_quote(s):
if not s:
return u"''"
if re.search(r'[^\w@%+=:,./-]', s) is None:
return s
return u"'" + s.replace(u"'", u"'\"'\"'") + u"'"
def build_all_include():
# Build a cpp file that includes all header files in this repo.
# Otherwise header-only integrations would not be tested by clang-tidy
headers = []
for path in walk_files(basepath):
filetypes = ('.h',)
ext = os.path.splitext(path)[1]
if ext in filetypes:
path = os.path.relpath(path, root_path)
include_p = path.replace(os.path.sep, '/')
headers.append('#include "{}"'.format(include_p))
headers.sort()
headers.append('')
content = '\n'.join(headers)
with codecs.open(temp_header_file, 'w', encoding='utf-8') as f:
f.write(content)
def build_compile_commands():
gcc_flags_json = os.path.join(root_path, '.gcc-flags.json')
if not os.path.isfile(gcc_flags_json):
print("Could not find {} file which is required for clang-tidy.")
print('Please run "pio init --ide atom" in the root esphome folder to generate that file.')
sys.exit(1)
with codecs.open(gcc_flags_json, 'r', encoding='utf-8') as f:
gcc_flags = json.load(f)
exec_path = gcc_flags['execPath']
include_paths = gcc_flags['gccIncludePaths'].split(',')
includes = ['-I{}'.format(p) for p in include_paths]
cpp_flags = gcc_flags['gccDefaultCppFlags'].split(' ')
defines = [flag for flag in cpp_flags if flag.startswith('-D')]
command = [exec_path]
command.extend(includes)
command.extend(defines)
command.append('-std=gnu++11')
command.append('-Wall')
command.append('-Wno-delete-non-virtual-dtor')
command.append('-Wno-unused-variable')
command.append('-Wunreachable-code')
source_files = []
for path in walk_files(basepath):
filetypes = ('.cpp',)
ext = os.path.splitext(path)[1]
if ext in filetypes:
source_files.append(os.path.abspath(path))
source_files.append(temp_header_file)
source_files.sort()
compile_commands = [{
'directory': root_path,
'command': ' '.join(shlex_quote(x) for x in (command + ['-o', p + '.o', '-c', p])),
'file': p
} for p in source_files]
compile_commands_json = os.path.join(root_path, 'compile_commands.json')
if os.path.isfile(compile_commands_json):
with codecs.open(compile_commands_json, 'r', encoding='utf-8') as f:
try:
if json.load(f) == compile_commands:
return
except:
pass
with codecs.open(compile_commands_json, 'w', encoding='utf-8') as f:
json.dump(compile_commands, f, indent=2)
def walk_files(path):
for root, _, files in os.walk(path):
for name in files:
yield os.path.join(root, name)
def get_output(*args):
proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = proc.communicate()
return output.decode('utf-8')
def splitlines_no_ends(string):
return [s.strip() for s in string.splitlines()]
def changed_files():
for remote in ('upstream', 'origin'):
command = ['git', 'merge-base', '{}/dev'.format(remote), 'HEAD']
try:
merge_base = splitlines_no_ends(get_output(*command))[0]
break
except:
pass
else:
raise ValueError("Git not configured")
command = ['git', 'diff', merge_base, '--name-only']
changed = splitlines_no_ends(get_output(*command))
changed = [os.path.relpath(f, os.getcwd()) for f in changed]
changed.sort()
return changed
def filter_changed(files):
changed = changed_files()
files = [f for f in files if f in changed]
print("Changed files:")
if not files:
print(" No changed files!")
for c in files:
print(" {}".format(c))
return files

View file

@ -12,5 +12,5 @@ fi
set -x
script/clang-tidy.py -c --fix --all-headers
script/clang-format.py -c -i
script/clang-tidy -c --fix --all-headers
script/clang-format -c -i

View file

@ -1,10 +1,74 @@
#!/usr/bin/env bash
#!/usr/bin/env python
set -e
from __future__ import print_function
cd "$(dirname "$0")/.."
set -x
import argparse
import collections
import os
import re
import sys
script/ci-custom.py
flake8 esphome
pylint esphome
sys.path.append(os.path.dirname(__file__))
from helpers import basepath, get_output, walk_files, filter_changed
def main():
parser = argparse.ArgumentParser()
parser.add_argument('files', nargs='*', default=[],
help='files to be processed (regex on path)')
parser.add_argument('-c', '--changed', action='store_true',
help='Only run on changed files')
args = parser.parse_args()
files = []
for path in walk_files(basepath):
filetypes = ('.py',)
ext = os.path.splitext(path)[1]
if ext in filetypes:
path = os.path.relpath(path, os.getcwd())
files.append(path)
# Match against re
file_name_re = re.compile('|'.join(args.files))
files = [p for p in files if file_name_re.search(p)]
if args.changed:
files = filter_changed(files)
files.sort()
errors = collections.defaultdict(list)
cmd = ['flake8'] + files
print("Running flake8...")
log = get_output(*cmd)
for line in log.splitlines():
line = line.split(':')
if len(line) < 4:
continue
file_ = line[0]
linno = line[1]
msg = (u':'.join(line[3:])).strip()
errors[file_].append(u'{}:{} - {}'.format(file_, linno, msg))
cmd = ['pylint', '-f', 'parseable', '--persistent=n'] + files
print("Running pylint...")
log = get_output(*cmd)
for line in log.splitlines():
line = line.split(':')
if len(line) < 3:
continue
file_ = line[0]
linno = line[1]
msg = (u':'.join(line[3:])).strip()
errors[file_].append(u'{}:{} - {}'.format(file_, linno, msg))
for f, errs in sorted(errors.items()):
print("\033[0;32m************* File \033[1;32m{}\033[0m".format(f))
for err in errs:
print(err)
print()
sys.exit(len(errors))
if __name__ == '__main__':
main()

11
script/quicklint Executable file
View file

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
set -x
script/ci-custom.py
script/lint-python -c
script/lint-cpp

Some files were not shown because too many files have changed in this diff Show more