From 68ce1b18c48db9d5589bd45b193421c0b4001973 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 28 May 2019 16:00:00 +0200 Subject: [PATCH] Add NTC and resistance sensor (#560) * Add NTC and resistance sensor Fixes https://github.com/esphome/feature-requests/issues/248 * Fix * Fix platformio4 moved get_project_dir --- esphome/__main__.py | 5 +- esphome/components/ntc/__init__.py | 0 esphome/components/ntc/ntc.cpp | 31 +++++ esphome/components/ntc/ntc.h | 29 +++++ esphome/components/ntc/sensor.py | 120 ++++++++++++++++++ esphome/components/resistance/__init__.py | 0 .../resistance/resistance_sensor.cpp | 42 ++++++ .../components/resistance/resistance_sensor.h | 38 ++++++ esphome/components/resistance/sensor.py | 37 ++++++ esphome/const.py | 1 + esphome/platformio_api.py | 6 +- tests/test3.yaml | 14 ++ 12 files changed, 321 insertions(+), 2 deletions(-) create mode 100644 esphome/components/ntc/__init__.py create mode 100644 esphome/components/ntc/ntc.cpp create mode 100644 esphome/components/ntc/ntc.h create mode 100644 esphome/components/ntc/sensor.py create mode 100644 esphome/components/resistance/__init__.py create mode 100644 esphome/components/resistance/resistance_sensor.cpp create mode 100644 esphome/components/resistance/resistance_sensor.h create mode 100644 esphome/components/resistance/sensor.py diff --git a/esphome/__main__.py b/esphome/__main__.py index 199b68a6f1..c48d49b9ac 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -127,7 +127,10 @@ def wrap_to_code(name, comp): 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')))) + conf_str = yaml_util.dump(conf) + if IS_PY2: + conf_str = conf_str.decode('utf-8') + cg.add(cg.LineComment(indent(conf_str))) yield coro(conf) return wrapped diff --git a/esphome/components/ntc/__init__.py b/esphome/components/ntc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ntc/ntc.cpp b/esphome/components/ntc/ntc.cpp new file mode 100644 index 0000000000..1b5c5182c7 --- /dev/null +++ b/esphome/components/ntc/ntc.cpp @@ -0,0 +1,31 @@ +#include "ntc.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ntc { + +static const char *TAG = "ntc"; + +void NTC::setup() { + this->sensor_->add_on_state_callback([this](float value) { this->process_(value); }); + if (this->sensor_->has_state()) + this->process_(this->sensor_->state); +} +void NTC::dump_config() { LOG_SENSOR("", "NTC Sensor", this) } +float NTC::get_setup_priority() const { return setup_priority::DATA; } +void NTC::process_(float value) { + if (isnan(value)) { + this->publish_state(NAN); + return; + } + + float lr = logf(value); + float v = this->a_ + this->b_ * lr + this->c_ * lr * lr * lr; + float temp = 1 / v - 273.15f; + + ESP_LOGD(TAG, "'%s' - Temperature: %.1f°C", this->name_.c_str(), temp); + this->publish_state(temp); +} + +} // namespace ntc +} // namespace esphome diff --git a/esphome/components/ntc/ntc.h b/esphome/components/ntc/ntc.h new file mode 100644 index 0000000000..9d6b37412d --- /dev/null +++ b/esphome/components/ntc/ntc.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace ntc { + +class NTC : public Component, public sensor::Sensor { + public: + void set_sensor(Sensor *sensor) { sensor_ = sensor; } + void set_a(float a) { a_ = a; } + void set_b(float b) { b_ = b; } + void set_c(float c) { c_ = c; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + protected: + void process_(float value); + + sensor::Sensor *sensor_; + float a_; + float b_; + float c_; +}; + +} // namespace ntc +} // namespace esphome diff --git a/esphome/components/ntc/sensor.py b/esphome/components/ntc/sensor.py new file mode 100644 index 0000000000..db7d7f9b99 --- /dev/null +++ b/esphome/components/ntc/sensor.py @@ -0,0 +1,120 @@ +# coding=utf-8 +from math import log + +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.components import sensor +from esphome.const import UNIT_CELSIUS, ICON_THERMOMETER, CONF_SENSOR, CONF_TEMPERATURE, \ + CONF_VALUE, CONF_CALIBRATION, CONF_ID + +ntc_ns = cg.esphome_ns.namespace('ntc') +NTC = ntc_ns.class_('NTC', cg.Component, sensor.Sensor) + +CONF_B_CONSTANT = 'b_constant' +CONF_REFERENCE_TEMPERATURE = 'reference_temperature' +CONF_REFERENCE_RESISTANCE = 'reference_resistance' +CONF_A = 'a' +CONF_B = 'b' +CONF_C = 'c' +ZERO_POINT = 273.15 + + +def validate_calibration_parameter(value): + if isinstance(value, dict): + return cv.Schema({ + cv.Required(CONF_TEMPERATURE): cv.float_, + cv.Required(CONF_VALUE): cv.float_, + }) + + value = cv.string(value) + parts = value.split('->') + if len(parts) != 2: + raise cv.Invalid(u"Calibration parameter must be of form 3000 -> 23°C") + voltage = cv.resistance(parts[0].strip()) + temperature = cv.temperature(parts[1].strip()) + return validate_calibration_parameter({ + CONF_TEMPERATURE: temperature, + CONF_VALUE: voltage, + }) + + +def calc_steinhart_hart(value): + r1 = value[0][CONF_VALUE] + r2 = value[1][CONF_VALUE] + r3 = value[2][CONF_VALUE] + t1 = value[0][CONF_TEMPERATURE] + ZERO_POINT + t2 = value[1][CONF_TEMPERATURE] + ZERO_POINT + t3 = value[2][CONF_TEMPERATURE] + ZERO_POINT + + l1 = log(r1) + l2 = log(r2) + l3 = log(r3) + + y1 = 1/t1 + y2 = 1/t2 + y3 = 1/t3 + + g2 = (y2-y1)/(l2-l1) + g3 = (y3-y1)/(l3-l1) + + c = (g3-g2)/(l3-l2) * 1/(l1+l2+l3) + b = g2 - c*(l1*l1 + l1*l2 + l2*l2) + a = y1 - (b + l1*l1*c) * l1 + return a, b, c + + +def calc_b(value): + beta = value[CONF_B_CONSTANT] + t0 = value[CONF_REFERENCE_TEMPERATURE] + ZERO_POINT + r0 = value[CONF_REFERENCE_RESISTANCE] + + a = (1/t0) - (1/beta) * log(r0) + b = 1/beta + c = 0 + + return a, b, c + + +def process_calibration(value): + if isinstance(value, dict): + value = cv.Schema({ + cv.Required(CONF_B_CONSTANT): cv.float_, + cv.Required(CONF_REFERENCE_TEMPERATURE): cv.temperature, + cv.Required(CONF_REFERENCE_RESISTANCE): cv.resistance, + })(value) + a, b, c = calc_b(value) + elif isinstance(value, list): + if len(value) != 3: + raise cv.Invalid("Steinhart–Hart Calibration must consist of exactly three values") + value = cv.Schema([validate_calibration_parameter])(value) + a, b, c = calc_steinhart_hart(value) + else: + raise cv.Invalid("Calibration parameter accepts either a list for steinhart-hart " + "calibration, or mapping for b-constant calibration, " + "not {}".format(type(value))) + + return { + CONF_A: a, + CONF_B: b, + CONF_C: c, + } + + +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ + cv.GenerateID(): cv.declare_id(NTC), + cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_CALIBRATION): process_calibration, +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield sensor.register_sensor(var, config) + + sens = yield cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_sensor(sens)) + calib = config[CONF_CALIBRATION] + cg.add(var.set_a(calib[CONF_A])) + cg.add(var.set_b(calib[CONF_B])) + cg.add(var.set_c(calib[CONF_C])) diff --git a/esphome/components/resistance/__init__.py b/esphome/components/resistance/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/resistance/resistance_sensor.cpp b/esphome/components/resistance/resistance_sensor.cpp new file mode 100644 index 0000000000..7c48bbbc08 --- /dev/null +++ b/esphome/components/resistance/resistance_sensor.cpp @@ -0,0 +1,42 @@ +#include "resistance_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace resistance { + +static const char *TAG = "resistance"; + +void ResistanceSensor::dump_config() { + LOG_SENSOR("", "Resistance Sensor", this); + ESP_LOGCONFIG(TAG, " Configuration: %s", this->configuration_ == UPSTREAM ? "UPSTREAM" : "DOWNSTREAM"); + ESP_LOGCONFIG(TAG, " Resistor: %.2fΩ", this->resistor_); + ESP_LOGCONFIG(TAG, " Reference Voltage: %.1fV", this->reference_voltage_); +} +void ResistanceSensor::process_(float value) { + if (isnan(value)) { + this->publish_state(NAN); + return; + } + float res = 0; + switch (this->configuration_) { + case UPSTREAM: + if (value == 0.0f) + res = NAN; + else + res = (this->reference_voltage_ - value) / value; + break; + case DOWNSTREAM: + if (value == this->reference_voltage_) + res = NAN; + else + res = value / (this->reference_voltage_ - value); + break; + } + + res *= this->resistor_; + ESP_LOGD(TAG, "'%s' - Resistance %.1fΩ", this->name_.c_str(), res); + this->publish_state(res); +} + +} // namespace resistance +} // namespace esphome diff --git a/esphome/components/resistance/resistance_sensor.h b/esphome/components/resistance/resistance_sensor.h new file mode 100644 index 0000000000..b57f90b59c --- /dev/null +++ b/esphome/components/resistance/resistance_sensor.h @@ -0,0 +1,38 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace resistance { + +enum ResistanceConfiguration { + UPSTREAM, + DOWNSTREAM, +}; + +class ResistanceSensor : public Component, public sensor::Sensor { + public: + void set_sensor(Sensor *sensor) { sensor_ = sensor; } + void set_configuration(ResistanceConfiguration configuration) { configuration_ = configuration; } + void set_resistor(float resistor) { resistor_ = resistor; } + void set_reference_voltage(float reference_voltage) { reference_voltage_ = reference_voltage; } + + void setup() override { + this->sensor_->add_on_state_callback([this](float value) { this->process_(value); }); + if (this->sensor_->has_state()) + this->process_(this->sensor_->state); + } + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void process_(float value); + sensor::Sensor *sensor_; + ResistanceConfiguration configuration_; + float resistor_; + float reference_voltage_; +}; + +} // namespace resistance +} // namespace esphome diff --git a/esphome/components/resistance/sensor.py b/esphome/components/resistance/sensor.py new file mode 100644 index 0000000000..fb245bcdf0 --- /dev/null +++ b/esphome/components/resistance/sensor.py @@ -0,0 +1,37 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import CONF_SENSOR, UNIT_OHM, ICON_FLASH, CONF_ID + +resistance_ns = cg.esphome_ns.namespace('resistance') +ResistanceSensor = resistance_ns.class_('ResistanceSensor', cg.Component, sensor.Sensor) + +CONF_REFERENCE_VOLTAGE = 'reference_voltage' +CONF_CONFIGURATION = 'configuration' +CONF_RESISTOR = 'resistor' + +ResistanceConfiguration = resistance_ns.enum('ResistanceConfiguration') +CONFIGURATIONS = { + 'DOWNSTREAM': ResistanceConfiguration.DOWNSTREAM, + 'UPSTREAM': ResistanceConfiguration.UPSTREAM, +} + +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_OHM, ICON_FLASH, 1).extend({ + cv.GenerateID(): cv.declare_id(ResistanceSensor), + cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_CONFIGURATION): cv.enum(CONFIGURATIONS, upper=True), + cv.Required(CONF_RESISTOR): cv.resistance, + cv.Optional(CONF_REFERENCE_VOLTAGE, default='3.3V'): cv.voltage, +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield sensor.register_sensor(var, config) + + sens = yield cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_sensor(sens)) + cg.add(var.set_configuration(config[CONF_CONFIGURATION])) + cg.add(var.set_resistor(config[CONF_RESISTOR])) + cg.add(var.set_reference_voltage(config[CONF_REFERENCE_VOLTAGE])) diff --git a/esphome/const.py b/esphome/const.py index 98e1505ee0..92b23f602f 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -62,6 +62,7 @@ CONF_BUILD_PATH = 'build_path' CONF_BUSY_PIN = 'busy_pin' CONF_BUS_VOLTAGE = 'bus_voltage' CONF_CALIBRATE_LINEAR = 'calibrate_linear' +CONF_CALIBRATION = 'calibration' CONF_CARRIER_DUTY_PERCENT = 'carrier_duty_percent' CONF_CARRIER_FREQUENCY = 'carrier_frequency' CONF_CHANGE_MODE_EVERY = 'change_mode_every' diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index cafb8ebaf4..adadab4109 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -19,12 +19,16 @@ def patch_structhash(): # all issues from platformio.commands import run from platformio import util + try: + from platformio.util import get_project_dir + except ImportError: + from platformio.project.helpers import get_project_dir from os.path import join, isdir, getmtime, isfile from os import makedirs def patched_clean_build_dir(build_dir): structhash_file = join(build_dir, "structure.hash") - platformio_ini = join(util.get_project_dir(), "platformio.ini") + platformio_ini = join(get_project_dir(), "platformio.ini") # if project's config is modified if isdir(build_dir) and getmtime(platformio_ini) > getmtime(build_dir): diff --git a/tests/test3.yaml b/tests/test3.yaml index 85dc714173..1b75dac7b1 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -115,6 +115,20 @@ sensor: - calibrate_linear: - 0 -> 0 - 100 -> 100 + - platform: resistance + sensor: my_sensor + configuration: DOWNSTREAM + resistor: 10kΩ + reference_voltage: 3.3V + name: Resistance + id: resist + - platform: ntc + sensor: resist + name: NTC Sensor + calibration: + b_constant: 3950 + reference_resistance: 10k + reference_temperature: 25°C - platform: tcs34725 red_channel: name: Red Channel