diff --git a/CODEOWNERS b/CODEOWNERS index ea3cdaed29..8c28f87977 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -343,6 +343,7 @@ esphome/components/mapping/* @clydebarrow esphome/components/matrix_keypad/* @ssieb esphome/components/max17043/* @blacknell esphome/components/max31865/* @DAVe3283 +esphome/components/max31888/* @JiriPrchal esphome/components/max44009/* @berfenger esphome/components/max6956/* @looping40 esphome/components/max7219digit/* @rspaargaren diff --git a/esphome/components/max31888/__init__.py b/esphome/components/max31888/__init__.py new file mode 100644 index 0000000000..880005d6e7 --- /dev/null +++ b/esphome/components/max31888/__init__.py @@ -0,0 +1,2 @@ +CODEOWNERS = ["@JiriPrchal"] +DOMAIN = "max31888" diff --git a/esphome/components/max31888/max31888.cpp b/esphome/components/max31888/max31888.cpp new file mode 100644 index 0000000000..5ff8e6f1ef --- /dev/null +++ b/esphome/components/max31888/max31888.cpp @@ -0,0 +1,95 @@ +#include "max31888.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::max31888 { + +ESPHOME_LOG_TAG(TAG, "max31888"); + +static constexpr uint32_t CONVERSION_TIMEOUT_ID = 0; + +static constexpr uint8_t MAX31888_COMMAND_START_CONVERSION = 0x44; +static constexpr uint8_t MAX31888_COMMAND_READ = 0x33; +static constexpr uint8_t MAX31888_COMMAND_SOFT_RESET = 0x82; +static constexpr uint8_t MAX31888_REGISTER_FIFO_DATA = 0x08; +static constexpr uint8_t MAX31888_FIFO_READ_LENGTH = 0x01; // number of bytes to read, minus one +static constexpr uint16_t MAX31888_CONVERSION_TIME_MS = 20; +static constexpr float MAX31888_DEGREES_PER_LSB = 0.005f; + +// Sends a command and checks the checksum the device appends to it (Maxim CRC-16 over the command byte, +// sent inverted and low byte first) +bool MAX31888Sensor::send_checked_command_(uint8_t command) { + if (!this->send_command_(command)) { + this->status_set_warning(LOG_STR("bus reset failed")); + return false; + } + const uint8_t low = this->bus_->read8(); + const uint8_t high = this->bus_->read8(); + if (crc16(&command, 1, 0, 0xa001, false, true) != encode_uint16(high, low)) { + this->status_set_warning(LOG_STR("command checksum invalid")); + return false; + } + return true; +} + +void MAX31888Sensor::setup() { + if (!this->check_address_or_index_()) + return; + if (!this->send_checked_command_(MAX31888_COMMAND_SOFT_RESET)) { + this->mark_failed(); + } +} + +void MAX31888Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "MAX31888 Sensor:"); + if (this->address_ == 0) { + ESP_LOGW(TAG, " Unable to select an address"); + return; + } + LOG_ONE_WIRE_DEVICE(this); + LOG_UPDATE_INTERVAL(this); +} + +void MAX31888Sensor::update() { + if (this->address_ == 0) + return; + + if (!this->send_checked_command_(MAX31888_COMMAND_START_CONVERSION)) { + this->publish_state(NAN); + return; + } + + this->set_timeout(CONVERSION_TIMEOUT_ID, MAX31888_CONVERSION_TIME_MS, [this] { + int16_t raw; + if (!this->read_temperature_(raw)) { + this->publish_state(NAN); + return; + } + const float temperature = raw * MAX31888_DEGREES_PER_LSB; + ESP_LOGD(TAG, "'%s': Got Temperature=%.3f°C", this->get_name().c_str(), temperature); + this->status_clear_warning(); + this->publish_state(temperature); + }); +} + +bool MAX31888Sensor::read_temperature_(int16_t &raw) { + // The checksum covers the command and its two arguments as well as the data + uint8_t frame[7] = {MAX31888_COMMAND_READ, MAX31888_REGISTER_FIFO_DATA, MAX31888_FIFO_READ_LENGTH}; + if (!this->send_command_(frame[0])) { + this->status_set_warning(LOG_STR("bus reset failed")); + return false; + } + this->bus_->write8(frame[1]); + this->bus_->write8(frame[2]); + for (size_t i = 3; i < sizeof(frame); i++) { + frame[i] = this->bus_->read8(); + } + if (crc16(frame, 5, 0, 0xa001, false, true) != encode_uint16(frame[6], frame[5])) { + this->status_set_warning(LOG_STR("checksum invalid")); + return false; + } + raw = static_cast(encode_uint16(frame[3], frame[4])); + return true; +} + +} // namespace esphome::max31888 diff --git a/esphome/components/max31888/max31888.h b/esphome/components/max31888/max31888.h new file mode 100644 index 0000000000..666f301cb0 --- /dev/null +++ b/esphome/components/max31888/max31888.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/one_wire/one_wire.h" + +namespace esphome::max31888 { + +class MAX31888Sensor final : public PollingComponent, public sensor::Sensor, public one_wire::OneWireDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + + protected: + bool send_checked_command_(uint8_t command); + /// Reads the latest conversion from the FIFO; false when the bus or the checksum fails + bool read_temperature_(int16_t &raw); +}; + +} // namespace esphome::max31888 diff --git a/esphome/components/max31888/sensor.py b/esphome/components/max31888/sensor.py new file mode 100644 index 0000000000..2809e7531a --- /dev/null +++ b/esphome/components/max31888/sensor.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +from esphome.components import one_wire, sensor +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) +from esphome.types import ConfigType + +max31888_ns = cg.esphome_ns.namespace("max31888") + +MAX31888Sensor = max31888_ns.class_( + "MAX31888Sensor", + cg.PollingComponent, + sensor.Sensor, + one_wire.OneWireDevice, +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + MAX31888Sensor, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(one_wire.one_wire_device_schema()) + .extend(cv.polling_component_schema("60s")) +) + + +async def to_code(config: ConfigType) -> None: + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await one_wire.register_one_wire_device(var, config) diff --git a/esphome/components/one_wire/one_wire_bus.cpp b/esphome/components/one_wire/one_wire_bus.cpp index 0a4880cf73..22d3afb775 100644 --- a/esphome/components/one_wire/one_wire_bus.cpp +++ b/esphome/components/one_wire/one_wire_bus.cpp @@ -10,6 +10,7 @@ static const uint8_t DALLAS_MODEL_DS1822 = 0x22; static const uint8_t DALLAS_MODEL_DS18B20 = 0x28; static const uint8_t DALLAS_MODEL_DS1825 = 0x3B; static const uint8_t DALLAS_MODEL_DS28EA00 = 0x42; +static const uint8_t MAXIM_MODEL_MAX31888 = 0x54; const uint8_t ONE_WIRE_ROM_SELECT = 0x55; const uint8_t ONE_WIRE_ROM_SEARCH = 0xF0; @@ -76,6 +77,8 @@ const LogString *OneWireBus::get_model_str(uint8_t model) { return LOG_STR("DS1825"); case DALLAS_MODEL_DS28EA00: return LOG_STR("DS28EA00"); + case MAXIM_MODEL_MAX31888: + return LOG_STR("MAX31888"); default: return LOG_STR("Unknown"); } diff --git a/tests/components/max31888/common.yaml b/tests/components/max31888/common.yaml new file mode 100644 index 0000000000..7b9ada2e82 --- /dev/null +++ b/tests/components/max31888/common.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: max31888 + one_wire_id: one_wire_bus + address: 0x1C0000031EDD2A54 + name: MAX31888 Temperature 1 + - platform: max31888 + one_wire_id: one_wire_bus + name: MAX31888 Temperature 2 + index: 1 diff --git a/tests/components/max31888/test.esp32-idf.yaml b/tests/components/max31888/test.esp32-idf.yaml new file mode 100644 index 0000000000..943a8c54a9 --- /dev/null +++ b/tests/components/max31888/test.esp32-idf.yaml @@ -0,0 +1,3 @@ +packages: + one_wire: !include ../../test_build_components/common/one_wire/esp32-idf.yaml + max31888: !include common.yaml diff --git a/tests/components/max31888/test.esp8266-ard.yaml b/tests/components/max31888/test.esp8266-ard.yaml new file mode 100644 index 0000000000..29ed6be26a --- /dev/null +++ b/tests/components/max31888/test.esp8266-ard.yaml @@ -0,0 +1,3 @@ +packages: + one_wire: !include ../../test_build_components/common/one_wire/esp8266-ard.yaml + max31888: !include common.yaml diff --git a/tests/components/max31888/test.rp2040-ard.yaml b/tests/components/max31888/test.rp2040-ard.yaml new file mode 100644 index 0000000000..80de52bae2 --- /dev/null +++ b/tests/components/max31888/test.rp2040-ard.yaml @@ -0,0 +1,3 @@ +packages: + one_wire: !include ../../test_build_components/common/one_wire/rp2040-ard.yaml + max31888: !include common.yaml diff --git a/tests/test_build_components/common/README.md b/tests/test_build_components/common/README.md index 010313db7f..4a89a1fb91 100644 --- a/tests/test_build_components/common/README.md +++ b/tests/test_build_components/common/README.md @@ -125,6 +125,11 @@ Components using `modbus` packages automatically get `uart` as well. - **RP2040**: SCL=GPIO5, SDA=GPIO4 - **BK72xx**: SCL=P20, SDA=P21 +### 1-Wire +- **ESP32 IDF**: GPIO4 +- **ESP8266**: GPIO13 +- **RP2040**: GPIO10 + ### I2C Low Frequency (10kHz) Same pin allocations as standard I2C, but with 10kHz frequency for components requiring slower speeds. diff --git a/tests/test_build_components/common/one_wire/esp32-idf.yaml b/tests/test_build_components/common/one_wire/esp32-idf.yaml new file mode 100644 index 0000000000..b34e87a758 --- /dev/null +++ b/tests/test_build_components/common/one_wire/esp32-idf.yaml @@ -0,0 +1,10 @@ +# Common 1-Wire configuration for esp32-idf tests +# Provides a shared 1-Wire bus that all components can use + +substitutions: + one_wire_pin: GPIO4 + +one_wire: + - platform: gpio + id: one_wire_bus + pin: ${one_wire_pin} diff --git a/tests/test_build_components/common/one_wire/esp8266-ard.yaml b/tests/test_build_components/common/one_wire/esp8266-ard.yaml new file mode 100644 index 0000000000..c4fefffd3e --- /dev/null +++ b/tests/test_build_components/common/one_wire/esp8266-ard.yaml @@ -0,0 +1,10 @@ +# Common 1-Wire configuration for esp8266-ard tests +# Provides a shared 1-Wire bus that all components can use + +substitutions: + one_wire_pin: GPIO13 + +one_wire: + - platform: gpio + id: one_wire_bus + pin: ${one_wire_pin} diff --git a/tests/test_build_components/common/one_wire/rp2040-ard.yaml b/tests/test_build_components/common/one_wire/rp2040-ard.yaml new file mode 100644 index 0000000000..8023264e40 --- /dev/null +++ b/tests/test_build_components/common/one_wire/rp2040-ard.yaml @@ -0,0 +1,10 @@ +# Common 1-Wire configuration for rp2040-ard tests +# Provides a shared 1-Wire bus that all components can use + +substitutions: + one_wire_pin: GPIO10 + +one_wire: + - platform: gpio + id: one_wire_bus + pin: ${one_wire_pin}