mirror of
https://github.com/esphome/esphome.git
synced 2024-11-25 16:38:16 +01:00
Merge branch 'dev' into ttp229-bsf
This commit is contained in:
commit
3e044db9f1
106 changed files with 1606 additions and 612 deletions
13
.gitignore
vendored
13
.gitignore
vendored
|
@ -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/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:])
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
|
@ -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_)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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); }
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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...");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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_; }
|
||||
|
|
103
esphome/components/sun/__init__.py
Normal file
103
esphome/components/sun/__init__.py
Normal 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
|
30
esphome/components/sun/sensor/__init__.py
Normal file
30
esphome/components/sun/sensor/__init__.py
Normal 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))
|
12
esphome/components/sun/sensor/sun_sensor.cpp
Normal file
12
esphome/components/sun/sensor/sun_sensor.cpp
Normal 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
|
41
esphome/components/sun/sensor/sun_sensor.h
Normal file
41
esphome/components/sun/sensor/sun_sensor.h
Normal 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
|
168
esphome/components/sun/sun.cpp
Normal file
168
esphome/components/sun/sun.cpp
Normal 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
|
137
esphome/components/sun/sun.h
Normal file
137
esphome/components/sun/sun.h
Normal 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
|
45
esphome/components/sun/text_sensor/__init__.py
Normal file
45
esphome/components/sun/text_sensor/__init__.py
Normal 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]))
|
12
esphome/components/sun/text_sensor/sun_text_sensor.cpp
Normal file
12
esphome/components/sun/text_sensor/sun_text_sensor.cpp
Normal 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
|
41
esphome/components/sun/text_sensor/sun_text_sensor.h
Normal file
41
esphome/components/sun/text_sensor/sun_text_sensor.h
Normal 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
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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'))
|
||||
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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'))
|
||||
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 ¤t, uint16_t begin, uint16_t end) {
|
||||
current++;
|
||||
|
@ -81,8 +83,18 @@ template<typename T> bool increment_time_value(T ¤t, 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
|
||||
|
|
|
@ -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); }
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -14,4 +14,3 @@
|
|||
}
|
||||
});
|
||||
})();
|
||||
|
|
@ -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.");
|
||||
|
|
|
@ -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
0
esphome/dashboard/static/materialize-stepper.min.css
vendored
Executable file → Normal file
0
esphome/dashboard/static/materialize-stepper.min.js
vendored
Executable file → Normal file
0
esphome/dashboard/static/materialize-stepper.min.js
vendored
Executable file → Normal file
|
@ -5,4 +5,3 @@ ace.define("ace/mode/yaml_highlight_rules",["require","exports","module","ace/li
|
|||
}
|
||||
});
|
||||
})();
|
||||
|
|
@ -5,4 +5,3 @@ ace.define("ace/theme/dreamweaver",["require","exports","module","ace/lib/dom"],
|
|||
}
|
||||
});
|
||||
})();
|
||||
|
|
@ -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
0
esphome/espota2.py
Executable file → Normal 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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
|
@ -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
128
script/helpers.py
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
11
script/quicklint
Executable 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
Loading…
Reference in a new issue