HomeAssistant integration of a Oregon Scientific WMR500C WiFi-enabled weather station.
By design, the weather station works as a standalone unit, while it also supports relaying the measurements to a cloud server, which in turn interacts with the official Android or iOS applications.
Hardware-wise, the device has no other usable external interfaces, besides the charge-only USB port and the WiFi interface.
Ever since the middle of 2021, the cloud services are no longer available, thus rendering the smartphone apps useless. Fortunately, part of these services can be masqueraded to allow local sampling of measurement values.
No (official) documentation regarding (third-party) integration is available - the information contained in this repo is based on reverse-engineering efforts.
Since reverse-engineering software may pose a infringement on copyrights, it is full responsibility of the user whether to reproduce the results or not.
In the following steps, the main unit will be referenced as WMR500
, or just device
.
The WMR500 relies on at least two cloud services, a HTTPS server app.idtlive.com
and a MQTT broker mqtt.idtlive.com
.
Since neither are available anymore, new ones need to be deployed locally, and WMR500's traffic to be redirected to them instead.
For traffic routing, a local static DNS entry is required - one method that doesn't depend on more advanced network routers is to:
- Install a DNS server on a user server, configuring it to assign DNS translation to local IP addresses,
- Configure the network router's DHCP server's advertised secondary DNS server to the user server IP address.
As a example, for a RaspberryPi4B+ assigned a IP address of 192.168.0.2
, connected to a router with LAN address 192.168.0.1
:
- Install a DNS server using:
sudo apt install dnsmasq
. - Configure the DNS server by adding the following lines to
/etc/dnsmasq.conf
:address=/app.idtlive.com/192.168.0.2 address=/mqtt.idtlive.com/192.168.0.2
- Optionally, to further improve network performance, add the following lines (only if the RaspberryPi is connected via wired Ethernet):
no-hosts no-resolv no-poll interface=eth0 no-dhcp-interface=eth0
- Reboot the RaspberryPi.
- On the main router set the secondary DNS server address to
192.168.0.2
- if necessary set the primary DNS server entry to the router's LAN IP address (for eg.192.168.0.1
). - All local devices that rely on DHCP IP address assignments will now have the two DNS server addresses advertised to,
192.168.0.2
(which will resolve onlyapp.idtlive.com
andmqtt.idtlive.com
) and192.168.0.1
(which will resolve all other DNS queries).
- (Optional) Reset the WMR500 to factory settings by holding both the
up
anddown
buttons on the unit for 6 seconds. - (Optional) Pair all external compatible sensors, by holding the
Pair
button, selecting option (2) using thedown
button, then pressingpair
. - Generate the Wifi configuration string using the script
wifi_auth_gen.py
- replace theWIFI_SSID
andWIFI_PASSWD
values inside the file with own WiFi credentials.
The config string has the structureWMR500C(xxAAAAA,yyBBBBB)
, whereAAAAA
is the SSID,xx
the number of characters in the SSID (excluding whitespaces),BBBBB
the password, andyy
the number of chars in the password, for example:WMR500C(04SSID,08PASSWORD)
. - Enter WiFi pairing by holding the
Pair
button, then pressingPair
again. - Connect any computer to the
OS_WMR500C_****
WiFi access point using WPA2 password12345678
. - Using a Telnet client, such as Putty or MobaXterm, connect to the server
192.168.10.1:50007
. - After sending the authentication string previously generated, the WMR500 responds with a ID and model name - take note of the first 36-chars value (GUUID, and also MQTT client ID), as it will be used in the next steps.
- Send the string
CONFIRM
to finalize the WiFi setup. - To allow the WMR500 to connect to the local MQTT server, its MQTT client password needs to be obtained - as it's using the unsecured MQTT protocol, it can easily be sniffed out using Wireshark's TShark command line utility, when run on the replacement server defined in chapter 1.
- Install the tool using
sudo apt install tshark
, then run it viasudo tshark -i eth0 -f "tcp port 1883" -Y 'mqtt.passwd' -V
to begin capturing all MQTT connect packets - modify the target network interface based on actual local server setup (for exampleeth0
for wired network, orwlan0
for wireless). - Trigger a full WiFi reconnection (cold-boot) by removing the batteries and USB power for at least 10 seconds, then replacing them.
- After around a minute, the packet analysis of a MQTT connection attempt should be displayed in the console - take note of the
Client ID
(same as GUUID) andPassword
values on the last lines.
If no MQTT connect packets are received, check that the local MQTT broker is running and its authentication method is configured with a password file or theallow_anonymous
option - use a desktop MQTT client to verify if connection with the set user/password credentials is actually possible. - Add the extracted authentication credentials to the MQTT broker's allowed users list.
- Confirm the WMR500 is connected for eg. by running command
netstat -ntp | grep ESTABLISHED.*mosquitto
if using a Mosquitto MQTT broker. If the command doesn't return any value, a restart of WMR500 and/or local server may be required. - Once the WMR500 is successfully connected to both WiFi and a local MQTT server, commands can be issued by any MQTT client that publishes to the
enno/out/json/_GUUID_
topic, where_GUUID_
is the 36-chars GUUID previously obtained. - The WMR500 reacts to commands by publishing its responses on the
enno/in/json
topic.
A number of non-volatile parameters can be set on the main unit, using the payload {"command": "setSettings", "XX": "YY", "id": "DEBUG"}
, where XX
is the parameter name, and YY
the new value. Known parameters are:
- `ca1`= temperature unit (integer): 0=dgrF, 1=dgrC.
- `ca2`= wind speed unit (integer): 0=m/s, 1=Knoten, 2=km/h, 3=mph.
- `ca3`= rainfall unit (integer): 0=mm, 1=inch.
- `ca4`= pressure unit (integer): 0=mbar, 1=hPa, 2=mmHg, 3=inHg.
- `ca5`= PM unit (integer): undefined.
- `ca6`= altitude (integer): meters.
- `cb1`= time zone (integer): undefined.
- `cb2`= time format (integer): 0=12H, 1=24H.
- `cb3`= language (integer): 0=EN, 1=FR, 2=GE, 3=IT, 4=ES, 5=RU.
- `cb4`= hemisphere (integer): 0=north, 1=south.
- `cb6`= latitude (double): degrees.
- `cb7`= longitude (double): degrees.
For example, to set the temperature unit to dgrC, publish to enno/out/json/_GUUID_
with payload {"command": "setSettings", "ca1": "1", "id": "DEBUG"}
.
To obtain the latest measurement values from the WMR500, publish to enno/out/json/_GUUID_
the payload {"command": "getChannel1Status", "id": "_GUUID_"}
(replace _GUUID_
with the 36-chars GUUID).
The WMR500 will publish the response to enno/in/json/
, with a JSON payload of a fixed structure, containing a number of keys, as shown below.
To ease in documenting the JSON contents, the numeric values have been replaced with a dictionary containing the label, data type, and unit ("_Label_", "_Type_", "_Unit_"
) for each known parameter - a number of _Comment_
key/value pairs were also added for clarity.
As a rule, the values of interest have the keys with the naming format of cXXX
, where XXX
is a 2-3 digit number.
{
"type": "m",
"correlationId": "_GUUID_",
"ts": "2016-01-01T00:00:00.000Z",
"deviceId": "_GUUID_",
"data": {
"6": {
"result": true,
"desc": "if false,return desc",
"indoor": {
"w8": { "_Comment_": "general",
"c81": {"_Label_" : "mac", "_Type_" : "String", "_Unit_": "no delimiters"},
"c82": {"_Label_" : "firmware_version", "_Type_" : "int", "_Unit_": "1490=default"},
"c83": {"_Label_" : "hardwareversion", "_Type_" : "int", "_Unit_": "1=default"},
"c84": {"_Label_" : "batteryIsLow", "_Type_" : "int", "_Unit_": "0=NO, 1=YES"},
"c85": {"_Label_" : "pairingMode", "_Type_" : "int", "_Unit_": "0=NO, 1=YES"},
"c86": {"_Label_" : "powerAdaptor", "_Type_" : "int", "_Unit_": "0=NO, 1=YES"},
"c87": {"_Label_" : "channel1status", "_Type_" : "int", "_Unit_": "0=NOK, 1=OK"},
"c88": {"_Label_" : "channel2status", "_Type_" : "int", "_Unit_": "0=NOK, 1=OK"},
"c89": {"_Label_" : "channel3status", "_Type_" : "int", "_Unit_": "0=NOK, 1=OK"},
"c811": {"_Label_" : "location", "_Type_" : "String", "_Unit_": "{latitute}, {longitude}"}
},
"w9": { "_Comment_": "indoor",
"c91": {"_Label_" : "temperature", "_Type_" : "float", "_Unit_": "dgrF, 210=NaN"},
"c92": {"_Label_" : "temperatureTrend", "_Type_" : "int", "_Unit_": "0=steady, 1=rise, 2=fall"},
"c93": {"_Label_" : "maxTemperatureToday", "_Type_" : "float", "_Unit_": "dgrF, 210=NaN"},
"c94": {"_Label_" : "minTemperatureToday", "_Type_" : "float", "_Unit_": "dgrF, 210=NaN"},
"c95": {"_Label_" : "humdityTrend", "_Type_" : "int", "_Unit_": "0=steady, 1=rise, 2=fall"},
"c96": {"_Label_" : "humdity", "_Type_" : "int", "_Unit_": "%, 210=NaN"},
"c97": {"_Label_" : "maxHumdityToday", "_Type_" : "int", "_Unit_": "%"},
"c98": {"_Label_" : "minHumdityToday", "_Type_" : "int", "_Unit_": "%"},
"c99": {"_Label_" : "heatIndex", "_Type_" : "float", "_Unit_": "dgrF, 210=NaN"},
"c911": {"_Label_" : "maxHeatIndexToday", "_Type_" : "float", "_Unit_": "dgrF, 210=NaN"},
"c912": {"_Label_" : "minHeatIndexToday", "_Type_" : "float", "_Unit_": "dgrF, 210=NaN"},
"c913": {"_Label_" : "dewPointTemperature", "_Type_" : "float", "_Unit_": "dgrF, 210=NaN"},
"c914": {"_Label_" : "maxDewPointTemperatureToday", "_Type_" : "float", "_Unit_": "dgrF"},
"c915": {"_Label_" : "minDewPointTemperatureToday", "_Type_" : "float", "_Unit_": "dgrF"}
},
"moonphase": {"_Label_" : "moonphase", "_Type_" : "int", "_Unit_": "0=firstquarter, 1=fullmoon, 2=newmoon, 3=thirdquarter, 4=waningcrescent, 5=waninggibbous, 6=waxingcrescent, 7=waxinggibbous"}
},
"outdoor": {
"channel1": {
"w7": { "_Comment_": "pm",
"c75": {"_Label_" : "undefined", "_Type_" : "int", "_Unit_": "undefined"},
"c77": {"_Label_" : "undefined", "_Type_" : "int", "_Unit_": "undefined"},
"c73": {"_Label_" : "undefined", "_Type_" : "int", "_Unit_": "undefined"},
"c71": {"_Label_" : "undefined", "_Type_" : "int", "_Unit_": "undefined"},
"c76": {"_Label_" : "undefined", "_Type_" : "int", "_Unit_": "undefined"},
"c74": {"_Label_" : "undefined", "_Type_" : "int", "_Unit_": "undefined"},
"c72": {"_Label_" : "undefined", "_Type_" : "int", "_Unit_": "undefined"}
},
"w1": { "_Comment_": "general",
"c17": {"_Label_" : "undefined", "_Type_" : "int", "_Unit_": "undefined"},
"c16": {"_Label_" : "undefined", "_Type_" : "int", "_Unit_": "undefined"},
"c14": {"_Label_" : "undefined", "_Type_" : "int", "_Unit_": "undefined"},
"c13": {"_Label_" : "undefined", "_Type_" : "int", "_Unit_": "undefined"},
"c11": {"_Label_" : "undefined", "_Type_" : "int", "_Unit_": "undefined"},
"c12": {"_Label_" : "undefined", "_Type_" : "int", "_Unit_": "undefined"},
"c15": {"_Label_" : "undefined", "_Type_" : "int", "_Unit_": "undefined"}
},
"w5": { "_Comment_": "pressure",
"c51": {"_Label_" : "weather_forecast", "_Type_" : "int", "_Unit_": "0=partly, 1=rainy, 2=cloudy, 3=sunny, 4=storm, 5=snow, 210=NaN"},
"c52": {"_Label_" : "pressure_trend", "_Type_" : "int", "_Unit_": "0=steady, 1=rise, 2=fall, 210=NaN"},
"c53": {"_Label_" : "pressure", "_Type_" : "float", "_Unit_": "mBar, 210=NaN"}
},
"w4": { "_Comment_": "rain",
"c41": {"_Label_" : "today_accumulated_rainfall", "_Type_" : "float", "_Unit_": "mm, 210=NaN"},
"c42": {"_Label_" : "rain_rate", "_Type_" : "float", "_Unit_": "mm/h, 210=NaN"},
"c43": {"_Label_" : "rain_rate_max", "_Type_" : "float", "_Unit_": "mm/h, 210=NaN"},
"c44": {"_Label_" : "past_accumulated_rainfall", "_Type_" : "float", "_Unit_": "mm, 210=NaN"}
},
"w3": { "_Comment_": "temperature_humidity",
"c31": {"_Label_" : "temperature_reading", "_Type_" : "float", "_Unit_": "dgrF, 210=NaN"},
"c32": {"_Label_" : "temperature_trend", "_Type_" : "int", "_Unit_": "0=steady, 1=rise, 2=fall"},
"c33": {"_Label_" : "temperature_max", "_Type_" : "float", "_Unit_": "dgrF, 210=NaN"},
"c34": {"_Label_" : "temperature_min", "_Type_" : "float", "_Unit_": "dgrF, 210=NaN"},
"c35": {"_Label_" : "humidity_reading", "_Type_" : "int", "_Unit_": "%, 210=NaN"},
"c36": {"_Label_" : "humidity_trend", "_Type_" : "int", "_Unit_": "0=steady, 1=rise, 2=fall"},
"c37": {"_Label_" : "humidity_max", "_Type_" : "int", "_Unit_": "%"},
"c38": {"_Label_" : "humidity_min", "_Type_" : "int", "_Unit_": "%"},
"c39": {"_Label_" : "heat_index", "_Type_" : "float", "_Unit_": "dgrF, 210=NaN"},
"c311": {"_Label_" : "heat_index_max", "_Type_" : "float", "_Unit_": "dgrF, 210=NaN"},
"c312": {"_Label_" : "heat_index_min", "_Type_" : "float", "_Unit_": "dgrF, 210=NaN"},
"c313": {"_Label_" : "dew_point_temperature", "_Type_" : "float", "_Unit_": "dgrF, 210=NaN"},
"c314": {"_Label_" : "dew_point_max", "_Type_" : "float", "_Unit_": "dgrF"},
"c315": {"_Label_" : "dew_point_min", "_Type_" : "float", "_Unit_": "dgrF"}
},
"w2": { "_Comment_": "wind",
"c21": {"_Label_" : "gust_wind_speed", "_Type_" : "float", "_Unit_": "m/s, 210=NaN"},
"c22": {"_Label_" : "average_wind_speed", "_Type_" : "float", "_Unit_": "m/s, 210=NaN"},
"c23": {"_Label_" : "gust_wind_direction", "_Type_" : "int", "_Unit_": "0 to 15, 0=N, 4=E, 8=S, 12=W, 210=NaN"},
"c24": {"_Label_" : "average_wind_direction", "_Type_" : "int", "_Unit_": "0 to 15, 0=N, 4=E, 8=S, 12=W"},
"c25": {"_Label_" : "dominant_direction_last", "_Type_" : "int", "_Unit_": "0 to 15, 0=N, 4=E, 8=S, 12=W"},
"c26": {"_Label_" : "wind_chill", "_Type_" : "float", "_Unit_": "dgrF, 210=NaN"},
"c27": {"_Label_" : "today_min_wind_chill", "_Type_" : "float", "_Unit_": "dgrF, 210=NaN"},
"c28": {"_Label_" : "wind_class", "_Type_" : "int", "_Unit_": "0=none, 1=light, 2=moderate, 3=strong, 4=storm, 210=NaN"},
"c29": {"_Label_" : "today_max_gust_wind_speed", "_Type_" : "float", "_Unit_": "m/s"}
}
}
}
}
}
}
For example, ["data"]["6"]["indoor"]["w9"]["c91"]
will contain the current indoor temperature.
To keep the WMR500 time and date synchronized, a HTTPS server is required to be deployed locally, so that a GET request to https://app.idtlive.com/api/time/iso_8601
shall be responded with a payload of format {"time":"2023-01-01 00:00:00+0"}
.
In order to masquerade the original HTTPS server, the official certificate private key is mandatory to sign the local server's TLS connection - unfortunately this is not possible due to obvious security issues and lack of support from manufacturer.
The only solution is to modify the embedded software (firmware) on the WMR500 base station, so that it either:
- Uses a different public key (and/or certificate) to authenticate the local server. the key (certificate) will need to be updated each time the server setup changes,
- Uses unsecured HTTP instead of HTTPS - no certification required, the local server can be (re)deployed without any further changes on the WMR500.
To perform the changes, the firmware onboard the WMR500's main microcontroller (STM32F411RE), needs to be patched, process which requires:
- Opening the case, by removing the two grey bezels on the front of the device, then unscrewing 6 screws under the outer bezel and one screw under the inner one,
- Soldering five wires to the testpoints available on the middle of the board - pinout from top to bottom:
VCC
(3.3V),SWDIO
,SWCLK
,RESET
(active-low), andGND
,
- Reading the firmware using a SWD-compatible flasher, such as a J-Link or any OpenOCD-compatible tool.
If using a J-Link, one may dump the full flash contents as a binary file by means of the included command-line utility via commandSaveBin C:\wmr500_firmware.bin 0x00 0x80000
. - Once the firmware is obtained, using the Ghidra tool for disassembly and analysis, parts of the instructions including those responsible for enabling TLS, are identified and patched.
See chapter 7 for reproducing locally the workspace setup.
- Flashing the modified firmware on the WMR500 will enable the changes.
Notes:
- To obtain firmware version number, either:
- check value of key
c82
in the response obtained in chapter 3, - hold
select
andup
buttons on the WMR500 for two seconds.
- check value of key
- A non-modified firmware dump version v1490 is included in this repo, besides an older version
1476
.
For a WMR500 that reports the firmware version as 1490
, there are two approaches to serving it the date and time, by either running a Python script:
- On any generic server/PC. If opening custom ports is not available in for eg. Home Assistant OS, this case requires an additional device to expose the service,
- Inside the AppDaemon add-on for Home Assistant. This option is best suited for running on standard HA installs such as RPi's.
Both approaches require at minimum the following binary firmware patches:
- Branch instruction (
BL
) at address0x0801b614
(responsible for TLS context initialization) to be replaced withNOP
, - Branch instruction (
BL
) at address0x0801b628
(responsible for TLS enabling) to be replaced withNOP
.
If using second approach, additional changes are necessary:
- Raw data bytes at address
0x080491a4
(used for issuing the HTTP request) to be replaced from47 45 54 20 2f 61 70 69 2f 74 69 6d 65 2f 69 73 6f 5f 38 36 30 31
to47 45 54 20 2F 61 70 69 2F 61 70 70 64 61 65 6D 6F 6E 2F 77 6D 72
.
As the Appdaemon REST API is limited, this patch replaces the default HTTP request URI from/api/time/iso_8601
to a supported one/api/appdaemon/wmr
. - Immediate value of Add (
ADDS
), at address0x0801b630
, to be replaced with2
.
The current Appdaemon implementation returns a JSON string with two whitespaces after the keys and values delimiter ("time":__"value"
) instead of one ("time":_"value"
), thus preventing the WMR500 in correctly parsing the values. Changing from1
to2
allows jumping to the correct start position of the JSON value.
Immediate value of Move Top instruction (MOVW
), at address 0x0801b630
(responsible with setting the HTTP server port number), is to be replaced with the desired value (1
to 65535
decimal). Factory default is 443
, while for the second approach the Appdaemon port is by default 5050
.
Two patched firmware images are present in this repo, for both first (port 443
) and second approach (port 5050
).
The following steps are applicable only for a WMR500 with a patched firmware, as per chapter 4.
If using the first approach (generic server):
- Install the required python libraries:
sudo pip install flask gunicorn
(why gunicorn?). - Optionally, edit the
http_wmr500_generic.py
file by configuring the HTTP port (HTTP_PORT
) as to the one patched on the WMR500 (default443
). - Run the Python script as root:
sudo gunicorn http_wmr500_generic:app -b 0.0.0.0:xxxx
, wherexxxx
is the chosen HTTP port.
If using the second approach (Appdaemon):
- Copy the
http_wmr500_appdaemon.py
file to the Appdaemon app folder (for eg./config/appdaemon/apps
in a Home Assistant OS installation). - Add the new module to app list file
apps.yaml
(present in the same folder as above), by appending the following lines:
http_wmr500:
module: http_wmr500_appdaemon
class: http_wmr500
- Wait for Appdaemon to automatically reload the new module, or restart it manually.
- Add the following lines in
automations.yaml
file (present in the same configuration folder).
Take note of the values_AUTOMATION_ID_
(random 13-digit value, unique to the automation),trigger
(whereminutes: /1
means every 60 seconds), and_GUUID_
(WMR500's GUUID).
- id: '_AUTOMATION_ID_'
alias: WMR500_Update_Trigger
description: ''
trigger:
- platform: time_pattern
minutes: /1
condition: []
action:
- service: mqtt.publish
data:
topic: enno/out/json/_GUUID_
payload: '{"command": "getChannel1Status", "id": "_GUUID_"}'
mode: single
- Add the following lines in
configuration.yaml
file (present inside the user-definedhomeassistant
configuration folder).
As the WMR500 reports a high number of measurements (over 55), user discretion is advised in selecting which measurement to be integrated in the HomeAssistant instance.
Note:expire_after
value should be set at least three times the sample period (as defined by the automation trigger period), for eg. 3*60=180.
mqtt:
sensor:
- name: INDOOR_TEMP
unique_id: "wmr500_indoor_temp"
state_topic: "enno/in/json"
value_template: "{{ value_json['data']['6']['indoor']['w9']['c91'] }}"
device_class: temperature
state_class: measurement
unit_of_measurement: "°F"
expire_after: 180
- name: INDOOR_HUMID
unique_id: "wmr500_indoor_humid"
state_topic: "enno/in/json"
value_template: "{{ value_json['data']['6']['indoor']['w9']['c96'] }}"
device_class: humidity
state_class: measurement
unit_of_measurement: "%"
expire_after: 180
- name: OUTDOOR_TEMP
unique_id: "wmr500_outdoor_temp"
state_topic: "enno/in/json"
value_template: "{{ value_json['data']['6']['outdoor']['channel1']['w3']['c31'] }}"
device_class: temperature
state_class: measurement
unit_of_measurement: "°F"
expire_after: 180
- name: OUTDOOR_HEAT_INDEX
unique_id: "wmr500_outdoor_heat_index"
state_topic: "enno/in/json"
value_template: "{{ value_json['data']['6']['outdoor']['channel1']['w3']['c39'] }}"
device_class: temperature
state_class: measurement
unit_of_measurement: "°F"
expire_after: 180
- name: OUTDOOR_DEW_POINT
unique_id: "wmr500_outdoor_dew_point"
state_topic: "enno/in/json"
value_template: "{{ value_json['data']['6']['outdoor']['channel1']['w3']['c313'] }}"
device_class: temperature
state_class: measurement
unit_of_measurement: "°F"
expire_after: 180
- name: OUTDOOR_WIND_CHILL
unique_id: "wmr500_outdoor_wind_chill"
state_topic: "enno/in/json"
value_template: "{{ value_json['data']['6']['outdoor']['channel1']['w2']['c26'] }}"
device_class: temperature
state_class: measurement
unit_of_measurement: "°F"
expire_after: 180
- name: OUTDOOR_HUMID
unique_id: "wmr500_outdoor_humid"
state_topic: "enno/in/json"
value_template: "{{ value_json['data']['6']['outdoor']['channel1']['w3']['c35'] }}"
device_class: humidity
state_class: measurement
unit_of_measurement: "%"
expire_after: 180
- name: OUTDOOR_WIND
unique_id: "wmr500_outdoor_wind"
state_topic: "enno/in/json"
value_template: "{{ ( value_json['data']['6']['outdoor']['channel1']['w2']['c21'] | float * 3.6 ) | round(2)}}"
device_class: wind_speed
state_class: measurement
unit_of_measurement: "km/h"
expire_after: 180
- name: OUTDOOR_RAIN
unique_id: "wmr500_outdoor_rain"
state_topic: "enno/in/json"
value_template: "{{ value_json['data']['6']['outdoor']['channel1']['w4']['c41'] }}"
device_class: distance
state_class: measurement
unit_of_measurement: "mm"
expire_after: 180
- name: OUTDOOR_PRESS
unique_id: "wmr500_outdoor_press"
state_topic: "enno/in/json"
value_template: "{{ value_json['data']['6']['outdoor']['channel1']['w5']['c53'] }}"
device_class: pressure
state_class: measurement
unit_of_measurement: "hPa"
expire_after: 180
- name: INDOOR_BATT
unique_id: "wmr500_indoor_batt"
state_topic: "enno/in/json"
value_template: >
{% set v = value_json['data']['6']['indoor']['w8']['c84'] %}
{% set n = 100 if v == 0 else 0 %}
{{ v }}
device_class: battery
state_class: measurement
unit_of_measurement: "%"
expire_after: 180
- name: OUTDOOR_BATT
unique_id: "wmr500_outdoor_batt"
state_topic: "enno/in/json"
value_template: >
{% set v = value_json['data']['6']['indoor']['w8']['c84'] %}
{% set n = 100 if v == 1 else 0 %}
{{ v }}
device_class: battery
state_class: measurement
unit_of_measurement: "%"
expire_after: 180
- If all is well, after a HA restart the newly created sensors shall be available.
All the previous steps were documented based on findings from decompiling/disassembly of both the Android app and the WMR500's firmware - as to be expected, there are many unknown features and also known issues still to be addressed.
One example is when the WMR500 randomly stops working correctly (no longer publishes on any MQTT topics), thus requiring a hardware reboot (power cycle).
Hardware-wise, the WMR500's main logic is controlled by a STM32F411RE microcontroller, complemented by a MX25L1606E SPI flash memory.
Based on memory content dumps, the external flash storage includes information such as user configuration (WiFi credentials, unit of measurements, etc.) and data statistics (min/max measurements, trends).
To further enhance the overall functionality by means of firmware analysis, one may setup a reverse-engineering environment, based on the Ghidra software solution.
Note: the following steps are for Ghidra version 10.4.
- Once installed and run, create a new non-shared Project via
File
->New project
. - Using
File
->Import File
, select the binary file dumped in a chapter 4. - Based on the targeted microcontroller, select
Language
asARM v7 32 little default
, then in theOptions
menu on the bottom-left set name toROM
and base address to80000000
, after which close both windows viaOk
. - Double-click the newly imported file to open the main development window - click
No
if asked to begin analyzing. - Via the
Window
top menu, selectMemory map
, then uncheck theW
checkbox for theROM
area. - Click the green plus button to add a new memory space with the following settings: name
RAM
, start address20000000
, length0x20000
, flagsread
,write
, andvolatile
.
- After closing the window, click
Analysis
->Auto Analyze
, leave all settings to default, then clickAnalyze
to begin disassembly of the source file. - Wait a few minutes until the process is completed - see the bottom-right progress bar.
- On the
Window
menu, one may browse through multiple views, including:Symbol tree
for functions (subroutines),Defined data
for variables and constants,Defined strings
for constant strings (char arrays).
- Best starting point may be going through the in-code usage of various key strings, focusing on those that include keywords related to the target functionality (for eg.
TLS
,socket
,connected
, etc.).
One notable hint is the string at address0x80051ad8
-Starting WICED v3.5.2
, which mentions the library used for networking; although deprecated around 2017, backups could still be available. - Using the library source files, one may cross-reference the function structures of known libraries to the disassembled code (which may not contain useful debug symbols such as function names).
- Another method of understanding the inner workings is through blind debugging of the firmware dump image - if a variable (or function) is found to be of interest, one may set a breakpoint on it to evaluate it's value (or call context).
- Finally, due to the design of the firmware, debugging printout is available via the hardware serial port (3.3V-only), accessible on-board the WMR500 through the
ML_TX
pin.
All the reverse-engineering, development, integration, and documentation efforts are based on the latest software and hardware versions available at the time of writing (November 2023), and licensed under the GNU General Public License v3.0.