/* * EMS-ESP - https://github.com/emsesp/EMS-ESP * Copyright 2020 Paul Derbyshire * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ // code originally written by nomis - https://github.com/nomis #include "dallassensor.h" #include "emsesp.h" #ifdef ESP32 #define YIELD #else #define YIELD yield() #endif namespace emsesp { uuid::log::Logger DallasSensor::logger_{F_(dallassensor), uuid::log::Facility::DAEMON}; // start the 1-wire void DallasSensor::start() { reload(); // disabled if dallas gpio is 0 if (dallas_gpio_) { #ifndef EMSESP_STANDALONE bus_.begin(dallas_gpio_); #endif // API calls Command::add_with_json( EMSdevice::DeviceType::DALLASSENSOR, F_(info), [&](const char * value, const int8_t id, JsonObject & json) { return command_info(value, id, json); }, F_(info_cmd)); Command::add_with_json( EMSdevice::DeviceType::DALLASSENSOR, F_(commands), [&](const char * value, const int8_t id, JsonObject & json) { return command_commands(value, id, json); }, F_(commands_cmd)); } } // load the MQTT settings void DallasSensor::reload() { EMSESP::webSettingsService.read([&](WebSettings & settings) { dallas_gpio_ = settings.dallas_gpio; parasite_ = settings.dallas_parasite; dallas_format_ = settings.dallas_format; }); if (Mqtt::ha_enabled()) { for (uint8_t i = 0; i < MAX_SENSORS; registered_ha_[i++] = false) ; } } void DallasSensor::loop() { if (!dallas_gpio_) { return; // dallas gpio is 0 (disabled) } #ifndef EMSESP_STANDALONE uint32_t time_now = uuid::get_uptime(); if (state_ == State::IDLE) { if (time_now - last_activity_ >= READ_INTERVAL_MS) { #ifdef EMSESP_DEBUG_SENSOR LOG_DEBUG(F("Read sensor temperature")); #endif if (bus_.reset() || parasite_) { YIELD; bus_.skip(); bus_.write(CMD_CONVERT_TEMP, parasite_ ? 1 : 0); state_ = State::READING; scanretry_ = 0; } else { // no sensors found if (sensors_.size()) { sensorfails_++; if (++scanretry_ > SCAN_MAX) { // every 30 sec scanretry_ = 0; LOG_ERROR(F("Bus reset failed")); for (auto & sensor : sensors_) { sensor.temperature_c = EMS_VALUE_SHORT_NOTSET; } } } } last_activity_ = time_now; } } else if (state_ == State::READING) { if (temperature_convert_complete() && (time_now - last_activity_ > CONVERSION_MS)) { #ifdef EMSESP_DEBUG_SENSOR LOG_DEBUG(F("Scanning for sensors")); #endif bus_.reset_search(); state_ = State::SCANNING; } else if (time_now - last_activity_ > READ_TIMEOUT_MS) { LOG_WARNING(F("Dallas sensor read timeout")); state_ = State::IDLE; sensorfails_++; } } else if (state_ == State::SCANNING) { if (time_now - last_activity_ > SCAN_TIMEOUT_MS) { LOG_ERROR(F("Dallas sensor scan timeout")); state_ = State::IDLE; sensorfails_++; } else { uint8_t addr[ADDR_LEN] = {0}; if (bus_.search(addr)) { if (!parasite_) { bus_.depower(); } if (bus_.crc8(addr, ADDR_LEN - 1) == addr[ADDR_LEN - 1]) { switch (addr[0]) { case TYPE_DS18B20: case TYPE_DS18S20: case TYPE_DS1822: case TYPE_DS1825: int16_t t; t = get_temperature_c(addr); if ((t >= -550) && (t <= 1250)) { sensorreads_++; // check if we have this sensor already bool found = false; for (auto & sensor : sensors_) { if (sensor.id() == get_id(addr)) { t += sensor.offset(); if (t != sensor.temperature_c) { sensor.temperature_c = t; changed_ |= true; } sensor.temperature_c = t; sensor.read = true; found = true; break; } } // add new sensor if (!found && (sensors_.size() < (MAX_SENSORS - 1))) { sensors_.emplace_back(addr); sensors_.back().temperature_c = t + sensors_.back().offset(); sensors_.back().read = true; changed_ = true; } } else { sensorfails_++; } break; default: sensorfails_++; LOG_ERROR(F("Unknown dallas sensor %s"), Sensor(addr).to_string().c_str()); break; } } else { sensorfails_++; LOG_ERROR(F("Invalid dallas sensor %s"), Sensor(addr).to_string().c_str()); } } else { if (!parasite_) { bus_.depower(); } // check for missing sensors after some samples if (++scancnt_ > SCAN_MAX) { for (auto & sensor : sensors_) { if (!sensor.read) { sensor.temperature_c = EMS_VALUE_SHORT_NOTSET; changed_ = true; } sensor.read = false; } scancnt_ = 0; } else if (scancnt_ == SCAN_START + 1) { // startup firstscan_ = sensors_.size(); LOG_DEBUG(F("Adding %d dallassensor(s) from first scan"), firstscan_); } else if ((scancnt_ <= 0) && (firstscan_ != sensors_.size())) { // check 2 times for no change of sensor # scancnt_ = SCAN_START; sensors_.clear(); // restart scaning and clear to get correct numbering } state_ = State::IDLE; } } } #endif } bool DallasSensor::temperature_convert_complete() { #ifndef EMSESP_STANDALONE if (parasite_) { return true; // don't care, use the minimum time in loop } return bus_.read_bit() == 1; #else return true; #endif } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" int16_t DallasSensor::get_temperature_c(const uint8_t addr[]) { #ifndef EMSESP_STANDALONE if (!bus_.reset()) { LOG_ERROR(F("Bus reset failed before reading scratchpad from %s"), Sensor(addr).to_string().c_str()); return EMS_VALUE_SHORT_NOTSET; } YIELD; uint8_t scratchpad[SCRATCHPAD_LEN] = {0}; bus_.select(addr); bus_.write(CMD_READ_SCRATCHPAD); bus_.read_bytes(scratchpad, SCRATCHPAD_LEN); YIELD; if (!bus_.reset()) { LOG_ERROR(F("Bus reset failed after reading scratchpad from %s"), Sensor(addr).to_string().c_str()); return EMS_VALUE_SHORT_NOTSET; } YIELD; if (bus_.crc8(scratchpad, SCRATCHPAD_LEN - 1) != scratchpad[SCRATCHPAD_LEN - 1]) { LOG_WARNING(F("Invalid scratchpad CRC: %02X%02X%02X%02X%02X%02X%02X%02X%02X from sensor %s"), scratchpad[0], scratchpad[1], scratchpad[2], scratchpad[3], scratchpad[4], scratchpad[5], scratchpad[6], scratchpad[7], scratchpad[8], Sensor(addr).to_string().c_str()); return EMS_VALUE_SHORT_NOTSET; } int16_t raw_value = ((int16_t)scratchpad[SCRATCHPAD_TEMP_MSB] << 8) | scratchpad[SCRATCHPAD_TEMP_LSB]; if (addr[0] == TYPE_DS18S20) { raw_value = (raw_value << 3) + 12 - scratchpad[SCRATCHPAD_CNT_REM]; } else { // Adjust based on sensor resolution int resolution = 9 + ((scratchpad[SCRATCHPAD_CONFIG] >> 5) & 0x3); switch (resolution) { case 9: raw_value &= ~0x7; break; case 10: raw_value &= ~0x3; break; case 11: raw_value &= ~0x1; break; case 12: break; } } raw_value = ((int32_t)raw_value * 625 + 500) / 1000; // round to 0.1 return raw_value; #else return EMS_VALUE_SHORT_NOTSET; #endif } #pragma GCC diagnostic pop const std::vector DallasSensor::sensors() const { return sensors_; } // skip crc from id. DallasSensor::Sensor::Sensor(const uint8_t addr[]) : id_(((uint64_t)addr[0] << 48) | ((uint64_t)addr[1] << 40) | ((uint64_t)addr[2] << 32) | ((uint64_t)addr[3] << 24) | ((uint64_t)addr[4] << 16) | ((uint64_t)addr[5] << 8) | ((uint64_t)addr[6])) { } uint64_t DallasSensor::get_id(const uint8_t addr[]) { return (((uint64_t)addr[0] << 48) | ((uint64_t)addr[1] << 40) | ((uint64_t)addr[2] << 32) | ((uint64_t)addr[3] << 24) | ((uint64_t)addr[4] << 16) | ((uint64_t)addr[5] << 8) | ((uint64_t)addr[6])); } uint64_t DallasSensor::Sensor::id() const { return id_; } std::string DallasSensor::Sensor::id_string() const { std::string str(20, '\0'); snprintf_P(&str[0], str.capacity() + 1, PSTR("%02X-%04X-%04X-%04X"), (unsigned int)(id_ >> 48) & 0xFF, (unsigned int)(id_ >> 32) & 0xFFFF, (unsigned int)(id_ >> 16) & 0xFFFF, (unsigned int)(id_)&0xFFFF); return str; } std::string DallasSensor::Sensor::to_string() const { std::string str = id_string(); EMSESP::webSettingsService.read([&](WebSettings & settings) { if (settings.dallas_format == Dallas_Format::NAME) { for (uint8_t i = 0; i < NUM_SENSOR_NAMES; i++) { if (strcmp(settings.sensor[i].id.c_str(), str.c_str()) == 0) { str = settings.sensor[i].name.c_str(); } } } }); return str; } int16_t DallasSensor::Sensor::offset() const { std::string str = id_string(); int16_t offset = 0; EMSESP::webSettingsService.read([&](WebSettings & settings) { for (uint8_t i = 0; i < NUM_SENSOR_NAMES; i++) { if (strcmp(settings.sensor[i].id.c_str(), str.c_str()) == 0) { offset = settings.sensor[i].offset; } } }); return offset; } void DallasSensor::add_name(const char * id, const char * name, int16_t offset) { EMSESP::webSettingsService.update([&](WebSettings & settings) { // check for new name of stored id for (uint8_t i = 0; i < NUM_SENSOR_NAMES; i++) { if (strcmp(id, settings.sensor[i].id.c_str()) == 0) { if (strlen(name) == 0 && offset == 0) { // delete entry if name and offset is empty settings.sensor[i].id = ""; settings.sensor[i].name = ""; settings.sensor[i].offset = 0; LOG_INFO(F("Deleting entry of sensor %s"), id); } else { settings.sensor[i].name = (strlen(name) == 0) ? id : name; settings.sensor[i].offset = offset; LOG_INFO(F("Setting name of sensor %s to %s"), id, name); } return StateUpdateResult::CHANGED; } } // check for free place for (uint8_t i = 0; i < NUM_SENSOR_NAMES; i++) { if (settings.sensor[i].id.isEmpty()) { settings.sensor[i].id = id; settings.sensor[i].name = (strlen(name) == 0) ? id : name; settings.sensor[i].offset = offset; LOG_INFO(F("Setting name of sensor %s to %s"), id, name); return StateUpdateResult::CHANGED; } } // check if there is a unused id and overwrite it for (uint8_t i = 0; i < NUM_SENSOR_NAMES; i++) { bool found = false; for (const auto & sensor : sensors_) { if (strcmp(sensor.id_string().c_str(), settings.sensor[i].id.c_str()) == 0) { found = true; } } if (!found) { settings.sensor[i].id = id; settings.sensor[i].name = (strlen(name) == 0) ? id : name; settings.sensor[i].offset = offset; LOG_INFO(F("Setting name of sensor %s to %s"), id, name); return StateUpdateResult::CHANGED; } } LOG_ERROR(F("List full, remove one sensorname first")); return StateUpdateResult::UNCHANGED; }, "local"); } // check to see if values have been updated bool DallasSensor::updated_values() { if (changed_) { changed_ = false; return true; } return false; } // list commands bool DallasSensor::command_commands(const char * value, const int8_t id, JsonObject & json) { return Command::list(EMSdevice::DeviceType::DALLASSENSOR, json); } // creates JSON doc from values // returns false if empty // e.g. dallassensor_data = {"sensor1":{"id":"28-EA41-9497-0E03-5F","temp":23.30},"sensor2":{"id":"28-233D-9497-0C03-8B","temp":24.0}} bool DallasSensor::command_info(const char * value, const int8_t id, JsonObject & json) { if (sensors_.size() == 0) { return false; } uint8_t i = 1; // sensor count for (const auto & sensor : sensors_) { char sensorID[10]; // sensor{1-n} snprintf_P(sensorID, 10, PSTR("sensor%d"), i++); if (id == -1) { // show number and id JsonObject dataSensor = json.createNestedObject(sensorID); dataSensor["id"] = sensor.to_string(); if (Helpers::hasValue(sensor.temperature_c)) { dataSensor["temp"] = (float)(sensor.temperature_c) / 10; } } else { // show according to format if (dallas_format_ == Dallas_Format::NUMBER && Helpers::hasValue(sensor.temperature_c)) { json[sensorID] = (float)(sensor.temperature_c) / 10; } else if (Helpers::hasValue(sensor.temperature_c)) { json[sensor.to_string()] = (float)(sensor.temperature_c) / 10; } } } return (json.size() > 0); } // send all dallas sensor values as a JSON package to MQTT void DallasSensor::publish_values(const bool force) { uint8_t num_sensors = sensors_.size(); if (num_sensors == 0) { return; } DynamicJsonDocument doc(100 * num_sensors); uint8_t sensor_no = 1; for (const auto & sensor : sensors_) { char sensorID[10]; // sensor{1-n} snprintf_P(sensorID, 10, PSTR("sensor%d"), sensor_no); if (dallas_format_ == Dallas_Format::NUMBER) { // e.g. dallassensor_data = {"sensor1":{"id":"28-EA41-9497-0E03","temp":23.3},"sensor2":{"id":"28-233D-9497-0C03","temp":24.0}} JsonObject dataSensor = doc.createNestedObject(sensorID); dataSensor["id"] = sensor.to_string(); if (Helpers::hasValue(sensor.temperature_c)) { dataSensor["temp"] = (float)(sensor.temperature_c) / 10; } } else if (Helpers::hasValue(sensor.temperature_c)) { doc[sensor.to_string()] = (float)(sensor.temperature_c) / 10; } // create the HA MQTT config // to e.g. homeassistant/sensor/ems-esp/dallas_28-233D-9497-0C03/config if (Mqtt::ha_enabled()) { if (!(registered_ha_[sensor_no - 1]) || force) { StaticJsonDocument config; config["dev_cla"] = FJSON("temperature"); char stat_t[50]; snprintf_P(stat_t, sizeof(stat_t), PSTR("%s/dallassensor_data"), Mqtt::base().c_str()); config["stat_t"] = stat_t; config["unit_of_meas"] = FJSON("°C"); char str[50]; if (dallas_format_ != Dallas_Format::NUMBER) { snprintf_P(str, sizeof(str), PSTR("{{value_json['%s']}}"), sensor.to_string().c_str()); } else { snprintf_P(str, sizeof(str), PSTR("{{value_json.sensor%d.temp}}"), sensor_no); } config["val_tpl"] = str; // name as sensor number not the long unique ID if (dallas_format_ != Dallas_Format::NUMBER) { snprintf_P(str, sizeof(str), PSTR("Dallas Sensor %s"), sensor.to_string().c_str()); } else { snprintf_P(str, sizeof(str), PSTR("Dallas Sensor %d"), sensor_no); } config["name"] = str; snprintf_P(str, sizeof(str), PSTR("dallas_%s"), sensor.to_string().c_str()); config["uniq_id"] = str; JsonObject dev = config.createNestedObject("dev"); JsonArray ids = dev.createNestedArray("ids"); ids.add("ems-esp"); char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; if (dallas_format_ == Dallas_Format::NUMBER) { snprintf_P(topic, sizeof(topic), PSTR("sensor/%s/dallas_sensor%d/config"), Mqtt::base().c_str(), sensor_no); } else { // use '_' as HA doesn't like '-' in the topic name std::string topicname = sensor.to_string(); std::replace(topicname.begin(), topicname.end(), '-', '_'); snprintf_P(topic, sizeof(topic), PSTR("sensor/%s/dallas_sensor%s/config"), Mqtt::base().c_str(), topicname.c_str()); } Mqtt::publish_ha(topic, config.as()); registered_ha_[sensor_no - 1] = true; } } sensor_no++; // increment sensor count } Mqtt::publish(F("dallassensor_data"), doc.as()); } } // namespace emsesp