diff --git a/README.md b/README.md index 1bc3c71..74381dc 100644 --- a/README.md +++ b/README.md @@ -176,12 +176,45 @@ Once connected, open `http://open-firenet.local` in a browser (or use the IP sho | Endpoint | Method | Description | |---|---|---| | `/api/status` | GET | JSON: wifi, ip, ssid, provisioning, mainLoop, controls | -| `/api/sensors` | GET | JSON: sensor fields from POST_SENSORS (f0 = room temp ×10) | +| `/api/sensors` | GET | JSON: stove sensors + stove controls state + ESP32 system fields (see below) | | `/api/controls` | GET | JSON: current setpoints (`onOff`, `operatingMode`, `heatingPower`, `tempRoomTarget`) | | `/api/controls` | POST | Set controls — body: `onOff=1; operatingMode=1; heatingPower=50; tempRoomTarget=210;` | | `/log` | GET | Plain-text log | | `/update` | GET/POST | OTA firmware update page | | `/reset-wifi` | GET | Erase WiFi credentials and reboot | +| `/restart` | GET | Reboot the ESP32 | + +### `/api/sensors` response fields + +All fields are always present; stove and controls fields are omitted until the first poll cycle completes. + +**Stove sensors** (from `POST_SENSORS`): + +| Field | Description | +|---|---| +| `f0` | Room temperature ×10 — e.g. `213` = 21.3 °C | +| `f1`–`f12` | Additional stove fields (see [PROTOCOL.md](PROTOCOL.md) for the full positional map) | + +**Stove controls state** (from `POST_CONTROLS`, present once the main loop has run at least one poll cycle): + +| Field | Description | +|---|---| +| `stoveOnOff` | Stove on/off as reported by stove (0 or 1) | +| `stoveOpMode` | Operating mode as reported by stove (0–3) | +| `stovePower` | Heating power % as reported by stove | +| `stoveTempTarget` | Temperature setpoint ×10 as reported by stove | + +**ESP32 system fields** (always present): + +| Field | Description | +|---|---| +| `uptime` | Seconds since boot | +| `firmware` | Firmware version string | +| `internalTemp` | ESP32 chip temperature in °C | +| `rssi` | WiFi signal strength in dBm (when connected) | +| `mac` | WiFi MAC address (when connected) | +| `ssid` | Connected SSID (when connected) | +| `ip` | Bridge IP address (when connected) | ### Controls format ``` @@ -238,28 +271,89 @@ Key facts: ## Home Assistant -Example REST sensor for room temperature: +The bridge exposes a local REST API — no cloud, no custom integration needed. Use `rest` sensors and `rest_command` in `configuration.yaml`. + +### Sensors ```yaml sensor: + # Room temperature from the stove (f0 is ×10) - platform: rest resource: http://open-firenet.local/api/sensors name: Stove room temperature value_template: "{{ (value_json.f0 | int / 10) | round(1) }}" unit_of_measurement: "°C" + device_class: temperature + scan_interval: 30 + + # Stove on/off state as reported by the stove + - platform: rest + resource: http://open-firenet.local/api/sensors + name: Stove state + value_template: "{{ value_json.stoveOnOff }}" + scan_interval: 30 + + # Current temperature setpoint (×10 → °C) + - platform: rest + resource: http://open-firenet.local/api/sensors + name: Stove temperature setpoint + value_template: "{{ (value_json.stoveTempTarget | int / 10) | round(1) }}" + unit_of_measurement: "°C" + device_class: temperature + scan_interval: 30 + + # Heating power % + - platform: rest + resource: http://open-firenet.local/api/sensors + name: Stove heating power + value_template: "{{ value_json.stovePower }}" + unit_of_measurement: "%" + scan_interval: 30 + + # Bridge WiFi signal + - platform: rest + resource: http://open-firenet.local/api/sensors + name: Open Firenet RSSI + value_template: "{{ value_json.rssi }}" + unit_of_measurement: "dBm" + device_class: signal_strength scan_interval: 60 +``` +### Commands + +```yaml rest_command: rika_start: url: "http://open-firenet.local/api/controls" method: POST payload: "onOff=1; operatingMode=1; heatingPower=50; tempRoomTarget=210;" content_type: text/plain + rika_stop: url: "http://open-firenet.local/api/controls" method: POST payload: "onOff=0; operatingMode=1; heatingPower=50; tempRoomTarget=210;" content_type: text/plain + + rika_set_temp: + url: "http://open-firenet.local/api/controls" + method: POST + # tempRoomTarget is ×10 — pass 210 for 21.0 °C + payload: "onOff=1; operatingMode=2; heatingPower=50; tempRoomTarget={{ (temp | float * 10) | int }};" + content_type: text/plain +``` + +### Automation example + +```yaml +automation: + - alias: Turn off stove at night + trigger: + - platform: time + at: "22:00:00" + action: + - service: rest_command.rika_stop ``` --- diff --git a/open-firenet/open-firenet.ino b/open-firenet/open-firenet.ino index ddbdc4c..f4b98aa 100644 --- a/open-firenet/open-firenet.ino +++ b/open-firenet/open-firenet.ino @@ -24,6 +24,7 @@ #define RIKA_PID 0x819A #define NTP_SERVER "pool.ntp.org" #define LOG_MAX_CHARS 32768 +#define FIRMWARE_VERSION "1.0.0" Preferences prefs; USBCDC USBSerial; @@ -394,13 +395,26 @@ void handleApiSensors() { if (i) json += ","; json += "\"" + sensors[i].key + "\":\"" + sensors[i].val + "\""; } - // Append values received from the stove (POST_CONTROLS parsed positionally) + // Stove controls state (POST_CONTROLS) if (stoveOnOff >= 0) { if (sensorCount) json += ","; - json += "\"stoveOnOff\":\"" + String(stoveOnOff) + "\""; - json += ",\"stoveOpMode\":\"" + String(stoveOpMode) + "\""; - json += ",\"stovePower\":\"" + String(stovePower) + "\""; - json += ",\"stoveTempTarget\":\"" + String(stoveTempTarget) + "\""; + json += "\"stoveOnOff\":" + String(stoveOnOff); + json += ",\"stoveOpMode\":" + String(stoveOpMode); + json += ",\"stovePower\":" + String(stovePower); + json += ",\"stoveTempTarget\":" + String(stoveTempTarget); + } + // ESP32 system sensors + bool needComma = sensorCount || stoveOnOff >= 0; + if (needComma) json += ","; + json += "\"uptime\":" + String(millis() / 1000); + json += ",\"firmware\":\"" FIRMWARE_VERSION "\""; + float intTemp = temperatureRead(); + json += ",\"internalTemp\":" + String(intTemp, 1); + if (WiFi.status() == WL_CONNECTED) { + json += ",\"mac\":\"" + WiFi.macAddress() + "\""; + json += ",\"rssi\":" + String(WiFi.RSSI()); + json += ",\"ssid\":\"" + wifiSsid + "\""; + json += ",\"ip\":\"" + WiFi.localIP().toString() + "\""; } json += "}"; server.send(200, "application/json", json); @@ -747,11 +761,23 @@ function sendCmd(e){ } function fetchAll(){ fetch('/api/sensors').then(function(r){return r.json();}).then(function(d){ - var rows=''; - if(d.f0!==undefined){var v0=parseInt(d.f0);rows+='';} - for(var k in d){if(k==='f0')continue;var v=parseInt(d[k]);var disp=(v>0&&v<5000)?((v/10).toFixed(1)+'°C'):(d[k]);rows+='';} + var STOVE_SKIP={'rssi':1,'ip':1,'mac':1,'ssid':1,'firmware':1,'uptime':1,'internalTemp':1,'stoveOnOff':1,'stoveOpMode':1,'stovePower':1,'stoveTempTarget':1}; + var rows='
'+t('sensors')+'
'+t('sensor_room')+''+(v0>0?(v0/10).toFixed(1)+'°C':'–')+'
'+k+''+disp+'
'; + // System sensors (always shown) + var uptime=d.uptime!==undefined?d.uptime:0; + var uh=Math.floor(uptime/3600),um=Math.floor((uptime%3600)/60),us=uptime%60; + var uptimeStr=(uh?uh+'h ':'')+(um?um+'m ':'')+us+'s'; + rows+=''; + if(d.internalTemp!==undefined) rows+=''; + if(d.rssi!==undefined) rows+=''; + if(d.uptime!==undefined) rows+=''; + if(d.firmware!==undefined) rows+=''; + // Stove sensors (f0, f1... from POST_SENSORS) + var stoveRows=0; + if(d.f0!==undefined){var v0=parseInt(d.f0);rows+='';rows+='';stoveRows++;} + for(var k in d){if(k==='f0'||STOVE_SKIP[k])continue;var v=parseInt(d[k]);var disp=(v>0&&v<5000&&String(d[k]).indexOf('.')<0)?((v/10).toFixed(1)+'°C'):(d[k]);rows+='';stoveRows++;} rows+='
ESP32
Temp. chip'+Number(d.internalTemp).toFixed(1)+'°C
WiFi RSSI'+d.rssi+' dBm
Uptime'+uptimeStr+'
Firmware'+d.firmware+'
'+t('sensors')+'
'+t('sensor_room')+''+(v0>0?(v0/10).toFixed(1)+'°C':'–')+'
'+k+''+disp+'
'; - document.getElementById('sensors').innerHTML=Object.keys(d).length?rows:''+t('waiting')+''; + document.getElementById('sensors').innerHTML=rows; }).catch(function(){}); fetch('/api/controls').then(function(r){return r.json();}).then(function(s){ if(Date.now()>ignoreCtrlUntil){ @@ -760,7 +786,7 @@ function fetchAll(){ document.getElementById('ctrl').textContent=disp; updateDisplay(); } - }); + }).catch(function(){}); if(logOpen)fetchLog(); } function copyLog(){var txt=document.getElementById('log').textContent;var el=document.createElement('textarea');el.value=txt;document.body.appendChild(el);el.select();document.execCommand('copy');document.body.removeChild(el);}