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='
| '+t('sensors')+' | |
';
- if(d.f0!==undefined){var v0=parseInt(d.f0);rows+='| '+t('sensor_room')+' | '+(v0>0?(v0/10).toFixed(1)+'°C':'–')+' |
';}
- 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+='| '+k+' | '+disp+' |
';}
+ 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='';
+ // 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+='| ESP32 |
';
+ if(d.internalTemp!==undefined) rows+='| Temp. chip | '+Number(d.internalTemp).toFixed(1)+'°C |
';
+ if(d.rssi!==undefined) rows+='| WiFi RSSI | '+d.rssi+' dBm |
';
+ if(d.uptime!==undefined) rows+='| Uptime | '+uptimeStr+' |
';
+ if(d.firmware!==undefined) rows+='| Firmware | '+d.firmware+' |
';
+ // Stove sensors (f0, f1... from POST_SENSORS)
+ var stoveRows=0;
+ if(d.f0!==undefined){var v0=parseInt(d.f0);rows+='| '+t('sensors')+' |
';rows+='| '+t('sensor_room')+' | '+(v0>0?(v0/10).toFixed(1)+'°C':'–')+' |
';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+='| '+k+' | '+disp+' |
';stoveRows++;}
rows+='
';
- 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);}