Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
################################################################################
# This .gitignore file was automatically created by Microsoft(R) Visual Studio.
################################################################################

.idea
.vs
.pio
pio
Expand Down
24 changes: 22 additions & 2 deletions HeishaMon/HeishaMon.ino
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,10 @@ void mqtt_reconnect()
sprintf_P(mqtt_topic, PSTR("%s/%s/WatthourTotal/2"), heishamonSettings.mqtt_topic_base, mqtt_topic_s0);
mqtt_client.subscribe(mqtt_topic);
}
if (heishamonSettings.use_1wire) { // allow setting the per-sensor calibration offset over mqtt: <base>/1wire/<address>/offset/set
sprintf_P(mqtt_topic, PSTR("%s/%s/+/offset/set"), heishamonSettings.mqtt_topic_base, mqtt_topic_1wire);
mqtt_client.subscribe(mqtt_topic);
}
if (heishamonSettings.use_1wire && mqttReconnects == 1) { // only on first connect: retrieve previously known 1wire sensors from their retained mqtt topics
sprintf_P(mqtt_topic, PSTR("%s/%s/+"), heishamonSettings.mqtt_topic_base, mqtt_topic_1wire);
mqtt_client.subscribe(mqtt_topic);
Expand Down Expand Up @@ -1030,8 +1034,12 @@ void mqtt_callback(char* topic, byte* payload, unsigned int length) {
mqttGPIOCallback(topic_gpiocommand, msg);
} else if (strncmp(topic_command, mqtt_topic_1wire, strlen(mqtt_topic_1wire)) == 0) { //this is a 1wire address topic, restore its retained value at boot
char* topic_1wire_address = topic_command + strlen(mqtt_topic_1wire) + 1; //strip the "1wire/" from the topic to get the sensor address
if ((strchr(topic_1wire_address, '/') == NULL) && (length > 0)) { //only handle the address topic itself (not .../alias) and skip empty (cleared) retained messages
char* topic_1wire_sub = strchr(topic_1wire_address, '/');
if ((topic_1wire_sub == NULL) && (length > 0)) { //only handle the address topic itself (not .../alias) and skip empty (cleared) retained messages
restoreDallasFromMqtt(topic_1wire_address, String(msg).toFloat(), log_message);
} else if ((topic_1wire_sub != NULL) && (strcmp_P(topic_1wire_sub, PSTR("/offset/set")) == 0) && (length > 0)) { //set the calibration offset of a sensor
*topic_1wire_sub = '\0'; //terminate the address (topiccopy is a private copy of the topic)
changeDallasOffset(mqtt_client, heishamonSettings.mqtt_topic_base, topic_1wire_address, msg, log_message);
}
}
free(topiccopy);
Expand Down Expand Up @@ -1090,6 +1098,8 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) {
client->route = 60;
} else if (strcmp((char *)dat, "/removedallas") == 0) {
client->route = 190;
} else if (strcmp((char *)dat, "/dallasoffset") == 0) {
client->route = 195;
} else if (strcmp((char *)dat, "/togglelog") == 0) {
client->route = 1;
log_message(_F("Toggled mqtt log flag"));
Expand Down Expand Up @@ -1192,6 +1202,12 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) {
removeDallasSensor(mqtt_client, heishamonSettings.mqtt_topic_base, (char *)args->name, log_message);
return 0;
} break;
case 195: {
char value[16];
snprintf(value, sizeof(value), "%.*s", (int)args->len, (char *)args->value);
changeDallasOffset(mqtt_client, heishamonSettings.mqtt_topic_base, (char *)args->name, value, log_message);
return 0;
} break;
case 100: {
unsigned char cmd[256] = { 0 };
char cpy[args->len + 1];
Expand Down Expand Up @@ -1336,6 +1352,9 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) {
case 190: {
return 0;
} break;
case 195: {
return 0;
} break;
case 80: {
if (client->content == 0) {
webserver_send(client, 302, (char *)"text/html", 0);
Expand Down Expand Up @@ -1481,7 +1500,8 @@ int8_t webserver_cb(struct webserver_t *client, void *dat) {
} break;
case 60:
case 70:
case 190: {
case 190:
case 195: {
header->ptr += sprintf_P((char *)header->buffer, PSTR("Location: /"));
return -1;
} break;
Expand Down
116 changes: 114 additions & 2 deletions HeishaMon/dallas.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ unsigned int updateAllDallasTime = 30000; // will be set using heishmonSettings
unsigned int dallasTimerWait = 30000; // will be set using heishmonSettings
void loadDallasAlias();
void saveDallasAliasFile();
void loadDallasOffset();
void saveDallasOffsetFile();

static int findDallasSlot(const char* address) {
for (int i = 0; i < MAX_DALLAS_SENSORS; i++) {
Expand Down Expand Up @@ -87,6 +89,7 @@ void rescanDallasSensors(void (*log_message)(char*), unsigned int dallasResoluti
actDallasData[slot].temperature = -127.0;
actDallasData[slot].lastgoodtime = 0;
strlcpy(actDallasData[slot].alias, "NOT SET", sizeof(actDallasData[slot].alias));
actDallasData[slot].offset = 0.0;
sprintf_P(log_msg, PSTR("Found new 1wire sensor: %s"), addrStr); log_message(log_msg);
sprintf_P(log_msg, PSTR("{\"data\": {\"dallasRescan\": true}}")); websocket_write_all(log_msg, strlen(log_msg)); // tell open browser tabs to reload the sensor table
}
Expand All @@ -103,6 +106,7 @@ void rescanDallasSensors(void (*log_message)(char*), unsigned int dallasResoluti

if (DALLASASYNC) DS18B20.setWaitForConversion(false); //async 1wire during next loops
loadDallasAlias();
loadDallasOffset();
}

void initDallasSensors(void (*log_message)(char*), unsigned int updateAllDallasTimeSettings, unsigned int dallasTimerWaitSettings, unsigned int dallasResolution) {
Expand Down Expand Up @@ -151,7 +155,9 @@ void readNewDallasTemp(PubSubClient &mqtt_client, void (*log_message)(char*), ch
websocket_write_all(log_msg, strlen(log_msg));
}
float allowedtempdiff = (((millis() - actDallasData[i].lastgoodtime)) / 1000.0) * MAXTEMPDIFFPERSEC;
if (fabs(temp - 85.0) < 0.0001) { // 85.0C is the DS18B20 power-on reset default, not a real reading; sensor is online, just not converted yet
bool powerOnReset = (fabs(temp - 85.0) < 0.0001); // check the raw value, before the calibration offset is applied
temp += actDallasData[i].offset; // apply the per-sensor calibration offset; everything below (filter, mqtt, json, rules) uses the corrected value
if (powerOnReset) { // 85.0C is the DS18B20 power-on reset default, not a real reading; sensor is online, just not converted yet
sprintf_P(log_msg, PSTR("Ignoring 1wire sensor power-on-reset value (85.00): %s"), actDallasData[i].address);
log_message(log_msg);
} else if ((actDallasData[i].temperature != -127.0) and ((temp > (actDallasData[i].temperature + allowedtempdiff)) or (temp < (actDallasData[i].temperature - allowedtempdiff)))) {
Expand All @@ -168,6 +174,8 @@ void readNewDallasTemp(PubSubClient &mqtt_client, void (*log_message)(char*), ch
sprintf_P(mqtt_topic, PSTR("%s/%s/%s"), mqtt_topic_base, mqtt_topic_1wire, actDallasData[i].address); mqtt_client.publish(mqtt_topic, valueStr, MQTT_RETAIN_VALUES);
sprintf_P(valueStr, PSTR("%s"), actDallasData[i].alias);
sprintf_P(mqtt_topic, PSTR("%s/%s/%s/alias"), mqtt_topic_base, mqtt_topic_1wire, actDallasData[i].address); mqtt_client.publish(mqtt_topic, valueStr, MQTT_RETAIN_VALUES);
sprintf_P(valueStr, PSTR("%.1f"), actDallasData[i].offset);
sprintf_P(mqtt_topic, PSTR("%s/%s/%s/offset"), mqtt_topic_base, mqtt_topic_1wire, actDallasData[i].address); mqtt_client.publish(mqtt_topic, valueStr, MQTT_RETAIN_VALUES);
} else {
sprintf_P(valueStr, PSTR("{\"Temperature\":%.2f,\"Alias\":\"%s\"}"), actDallasData[i].temperature, actDallasData[i].alias);
sprintf_P(mqtt_topic, PSTR("%s/%s/%s"), mqtt_topic_base, mqtt_topic_1wire, actDallasData[i].address); mqtt_client.publish(mqtt_topic, valueStr, MQTT_RETAIN_VALUES);
Expand Down Expand Up @@ -214,7 +222,10 @@ void dallasJsonOutput(struct webserver_t *client) {
webserver_send_content(client, str, strlen(str));
webserver_send_content_P(client, PSTR(",\"Alias\":\""), 10);
webserver_send_content(client, actDallasData[i].alias, strlen(actDallasData[i].alias));
webserver_send_content_P(client, PSTR("\",\"Present\":"), 12);
webserver_send_content_P(client, PSTR("\",\"Offset\":"), 11);
dtostrf(actDallasData[i].offset, 0, 1, str);
webserver_send_content(client, str, strlen(str));
webserver_send_content_P(client, PSTR(",\"Present\":"), 11);
if (actDallasData[i].present) {
webserver_send_content_P(client, PSTR("true"), 4);
} else {
Expand Down Expand Up @@ -260,11 +271,13 @@ void removeDallasSensor(PubSubClient &mqtt_client, char* mqtt_topic_base, char*
// publishing an empty retained payload clears the previously retained message on the broker
sprintf_P(mqtt_topic, PSTR("%s/%s/%s"), mqtt_topic_base, mqtt_topic_1wire, address); mqtt_client.publish(mqtt_topic, "", true);
sprintf_P(mqtt_topic, PSTR("%s/%s/%s/alias"), mqtt_topic_base, mqtt_topic_1wire, address); mqtt_client.publish(mqtt_topic, "", true);
sprintf_P(mqtt_topic, PSTR("%s/%s/%s/offset"), mqtt_topic_base, mqtt_topic_1wire, address); mqtt_client.publish(mqtt_topic, "", true);

sprintf_P(log_msg, PSTR("Removed 1wire sensor: %s"), address); log_message(log_msg);

actDallasData[slot] = dallasDataStruct();
saveDallasAliasFile();
saveDallasOffsetFile();
sprintf_P(log_msg, PSTR("{\"data\": {\"dallasRescan\": true}}")); websocket_write_all(log_msg, strlen(log_msg)); // tell open browser tabs to reload the sensor table
}

Expand All @@ -281,6 +294,7 @@ void restoreDallasFromMqtt(char* address, float temperature, void (*log_message)
strlcpy(actDallasData[slot].alias, "NOT SET", sizeof(actDallasData[slot].alias));
sprintf_P(log_msg, PSTR("Restored previously known 1wire sensor from mqtt: %s"), address); log_message(log_msg);
loadDallasAlias();
loadDallasOffset();
sprintf_P(log_msg, PSTR("{\"data\": {\"dallasRescan\": true}}")); websocket_write_all(log_msg, strlen(log_msg)); // tell open browser tabs to reload the sensor table
}
if (actDallasData[slot].lastgoodtime == 0) { //only backfill if we haven't taken a real reading yet this boot
Expand Down Expand Up @@ -308,3 +322,101 @@ void loadDallasAlias() {
}
}
}

// Calibration offsets are stored in their own file instead of being added to dallas.json,
// so a downgrade to an older firmware (which expects plain alias strings in dallas.json) keeps working.
void saveDallasOffsetFile() {
JsonDocument jsonDoc;
for (int i = 0 ; i < dallasDevicecount; i++) {
if (!actDallasData[i].known) continue;
if (actDallasData[i].offset == 0.0) continue; // only store sensors that actually have an offset
jsonDoc[actDallasData[i].address] = actDallasData[i].offset;
}
if (LittleFS.begin()) {
File configFile = LittleFS.open("/dallasoffset.json", "w");
if (configFile) {
serializeJson(jsonDoc, configFile);
configFile.close();
}
}
}

// rounds to 0.1 degree steps and clamps to +/- DALLAS_OFFSET_MAX
static float sanitizeDallasOffset(float offset) {
offset = roundf(offset * 10.0) / 10.0;
if (offset > DALLAS_OFFSET_MAX) offset = DALLAS_OFFSET_MAX;
if (offset < -DALLAS_OFFSET_MAX) offset = -DALLAS_OFFSET_MAX;
if (offset == 0.0) offset = 0.0; // normalize -0.0
return offset;
}

void loadDallasOffset() {
if (LittleFS.begin()) {
if (LittleFS.exists("/dallasoffset.json")) {
File configFile = LittleFS.open("/dallasoffset.json", "r");
if (configFile) {
JsonDocument jsonDoc;
DeserializationError error = deserializeJson(jsonDoc, configFile);
configFile.close();
if (!error) {
for (int i = 0 ; i < dallasDevicecount; i++) {
if (!actDallasData[i].known) continue;
if (jsonDoc[actDallasData[i].address].is<float>()) {
actDallasData[i].offset = sanitizeDallasOffset(jsonDoc[actDallasData[i].address].as<float>());
}
}
}
}
}
}
}

bool changeDallasOffset(PubSubClient &mqtt_client, char* mqtt_topic_base, char* address, const char* value, void (*log_message)(char*)) {
char log_msg[256];
char mqtt_topic[256];
char valueStr[16];

int slot = findDallasSlot(address);
if (slot < 0) {
sprintf_P(log_msg, PSTR("Cannot set offset, unknown 1wire sensor: %s"), address); log_message(log_msg);
return false;
}
// from here on use the stored address: the caller's buffer may be reused by an mqtt publish

// accept both "0.3" and "0,3"
char buf[16];
strlcpy(buf, value, sizeof(buf));
for (char* p = buf; *p; p++) {
if (*p == ',') *p = '.';
}
char* end = NULL;
float newOffset = strtof(buf, &end);
while (end && (*end == ' ')) end++;
if ((end == buf) || (end && *end != '\0') || isnan(newOffset) || isinf(newOffset)) {
sprintf_P(log_msg, PSTR("Invalid 1wire offset value for %s: %s"), actDallasData[slot].address, value); log_message(log_msg);
return false;
}
newOffset = sanitizeDallasOffset(newOffset);

float diff = newOffset - actDallasData[slot].offset;
actDallasData[slot].offset = newOffset;
saveDallasOffsetFile();
sprintf_P(log_msg, PSTR("1wire sensor %s offset set to %.1f"), actDallasData[slot].address, newOffset); log_message(log_msg);

sprintf_P(valueStr, PSTR("%.1f"), newOffset);
sprintf_P(mqtt_topic, PSTR("%s/%s/%s/offset"), mqtt_topic_base, mqtt_topic_1wire, actDallasData[slot].address); mqtt_client.publish(mqtt_topic, valueStr, MQTT_RETAIN_VALUES);

if (actDallasData[slot].temperature != -127.0) {
// shift the last known value right away, so mqtt, web and rules don't have to wait for the next reading
actDallasData[slot].temperature += diff;
sprintf_P(valueStr, PSTR("%.2f"), actDallasData[slot].temperature);
sprintf_P(mqtt_topic, PSTR("%s/%s/%s"), mqtt_topic_base, mqtt_topic_1wire, actDallasData[slot].address); mqtt_client.publish(mqtt_topic, valueStr, MQTT_RETAIN_VALUES);
sprintf_P(log_msg, PSTR("{\"data\": {\"dallasvalues\": {\"sensorID\": \"%s\", \"value\": %.2f, \"offset\": %.1f}}}"), actDallasData[slot].address, actDallasData[slot].temperature, newOffset);
websocket_write_all(log_msg, strlen(log_msg));
rules_event_cb(_F("ds18b20#"), actDallasData[slot].address);
} else {
sprintf_P(log_msg, PSTR("{\"data\": {\"dallasvalues\": {\"sensorID\": \"%s\", \"offset\": %.1f}}}"), actDallasData[slot].address, newOffset);
websocket_write_all(log_msg, strlen(log_msg));
}
return true;
}
3 changes: 3 additions & 0 deletions HeishaMon/dallas.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "src/common/webserver.h"

#define MAX_DALLAS_SENSORS 15
#define DALLAS_OFFSET_MAX 10.0 // max absolute calibration offset in degrees Celsius (set in steps of 0.1)
#if defined(ESP8266)
#define ONE_WIRE_BUS 4
#elif defined(ESP32)
Expand All @@ -21,6 +22,7 @@ struct dallasDataStruct {
DeviceAddress sensor;
char address[17] = "";
char alias[32] = "NOT SET";
float offset = 0.0; // calibration offset (degrees Celsius) added to every raw reading of this sensor
};

void resetlastalldatatime_dallas();
Expand All @@ -29,6 +31,7 @@ void initDallasSensors(void (*log_message)(char*), unsigned int updataAllDallasT
void rescanDallasSensors(void (*log_message)(char*), unsigned int dallasResolution);
void dallasJsonOutput(struct webserver_t *client);
void changeDallasAlias(char* address, char* alias);
bool changeDallasOffset(PubSubClient &mqtt_client, char* mqtt_topic_base, char* address, const char* value, void (*log_message)(char*));
void removeDallasSensor(PubSubClient &mqtt_client, char* mqtt_topic_base, char* address, void (*log_message)(char*));
void restoreDallasFromMqtt(char* address, float temperature, void (*log_message)(char*));

Expand Down
43 changes: 40 additions & 3 deletions HeishaMon/htmlcode.h
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,16 @@ input:disabled + .theme-slider-compact {
background:var(--bg-elevated);
}
.alias-edit:focus{border-color:var(--border-focus);box-shadow:0 0 0 2px var(--accent-glow)}
.offset-edit{
width:64px;
padding:3px 6px;
font-size:12.5px;
color:var(--text-secondary);
background:transparent;
border:1px solid var(--border);
border-radius:var(--radius-sm);
}
.offset-edit:focus{outline:none;border-color:var(--border-focus);box-shadow:0 0 0 2px var(--accent-glow)}
.tab-pane{display:none}
.tab-pane.active{display:block}
.settings-grid{
Expand Down Expand Up @@ -884,6 +894,10 @@ function startWebsockets(){
var dID=j.data.dallasvalues.sensorID;
if(j.data.dallasvalues.value!==undefined)updCell('SensorID-'+dID+'-Temperature',j.data.dallasvalues.value);
if(j.data.dallasvalues.present!==undefined)updDallasPresence(dID,j.data.dallasvalues.present);
if(j.data.dallasvalues.offset!==undefined){
var oi=document.getElementById('SensorID-'+dID+'-OffsetInput');
if(oi&&document.activeElement!==oi)oi.value=Number(j.data.dallasvalues.offset).toFixed(1);
}
} else if(j.data.s0values){
updCell('s0port-'+j.data.s0values.s0port+'-Watt',j.data.s0values.Watt);
updCell('s0port-'+j.data.s0values.s0port+'-Watthour',j.data.s0values.Watthour);
Expand Down Expand Up @@ -1036,6 +1050,17 @@ var dallasAliasEdit=function(){
xhr.open('GET','/dallasalias?'+addr+'='+alias,true);
xhr.send();
};
var dallasOffsetEdit=function(){
isEditing=false;
var addr=this.getAttribute('data-address');
var v=parseFloat(String(this.value).replace(',','.'));
if(isNaN(v))return;
v=Math.max(-10,Math.min(10,Math.round(v*10)/10));
this.value=v.toFixed(1);
var xhr=new XMLHttpRequest();
xhr.open('GET','/dallasoffset?'+addr+'='+encodeURIComponent(v.toFixed(1)),true);
xhr.send();
};
function rescanDallas(){
var xhr=new XMLHttpRequest();
xhr.open('GET','/scandallas',true);
Expand All @@ -1056,7 +1081,7 @@ function renderDallasTable(d){
var row=document.createElement('tr');
var sID=item['Sensor'];
if(!item.Present)row.style.opacity='0.6';
['Sensor','Temperature','Alias'].forEach(function(k){
['Sensor','Temperature','Offset','Alias'].forEach(function(k){
var cell=document.createElement('td');
cell.id='SensorID-'+sID+'-'+k;
if(k==='Alias'){
Expand All @@ -1068,6 +1093,18 @@ function renderDallasTable(d){
div.addEventListener('focus',function(){isEditing=true;});
div.addEventListener('blur',dallasAliasEdit);
cell.appendChild(div);
} else if(k==='Offset'){
var inp=document.createElement('input');
inp.type='number';inp.step='0.1';inp.min='-10';inp.max='10';
inp.value=(item.Offset!==undefined?Number(item.Offset):0).toFixed(1);
inp.className='offset-edit';
inp.id='SensorID-'+sID+'-OffsetInput';
inp.title='Calibration offset in °C, added to the raw sensor reading';
inp.setAttribute('data-address',item.Sensor);
inp.addEventListener('focus',function(){isEditing=true;});
inp.addEventListener('blur',function(){isEditing=false;});
inp.addEventListener('change',dallasOffsetEdit);
cell.appendChild(inp);
} else {cell.textContent=item[k];}
row.appendChild(cell);
});
Expand Down Expand Up @@ -1320,9 +1357,9 @@ static const char webBodyRootDallasValues[] FLASHPROG = R"====(
<div class='panel'>
<div class='panel-header'><h3>Dallas 1-Wire Sensors</h3><button onclick='rescanDallas()' class='btn btn-ghost' style='padding:4px 10px;font-size:11px;height:24px;'>&#8635; Rescan bus</button></div>
<table><thead><tr>
<th>Sensor</th><th>Temperature</th><th>Alias</th><th>Status</th><th></th>
<th>Sensor</th><th>Temperature</th><th>Offset (&deg;C)</th><th>Alias</th><th>Status</th><th></th>
</tr></thead><tbody id='dallasvalues'>
<tr><td colspan='5' style='color:var(--text-muted);padding:24px;text-align:center'>Loading…</td></tr>
<tr><td colspan='6' style='color:var(--text-muted);padding:24px;text-align:center'>Loading…</td></tr>
</tbody></table>
</div></div>
)====";
Expand Down
Loading
Loading