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
.
Since the integration relies on non-standard libraries, a Home Assistant Docker installation is assumed to be already working.
Also, a MQTT broker (for example Mosquitto) is also installed, configured and accessible in HA.
The WMR500's main base 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, a RaspberryPi4B+ running HomeAssistant, and assigned a IP address of 192.168.0.2, connected to a router with LAN address 192.168.0.1:
- Install on the RaspberryPi 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 only
app.idtlive.com
andmqtt.idtlive.com
) and 192.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 the previous chapter.
- 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.
Note - 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 possible. - Add the extracted authentication credentials to the MQTT broker's allowed users list.
- Confirm the WMR500 is connected by running the command:
netstat -ntp | grep ESTABLISHED.*mosquitto
(only if using a Mosquitto MQTT broker).
Note - 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"}
, whereXX
is the parameter name, andYY
the new value.
Known parameters are:ca1
= temperature unit (integer): 0=°F, 1=°C.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 °C, publish toenno/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 documenting the JSON contents, the numeric values have been replaced with a dictionary containing the label, data type, and unit for each known parameter - a number of _COMMENT_
key/value pairs were added to improve 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": "°F, 210=NaN"},
"c92": {"label" : "temperatureTrend", "type" : "int", "unit": "0=steady, 1=rise, 2=fall"},
"c93": {"label" : "maxTemperatureToday", "type" : "float", "unit": "°F, 210=NaN"},
"c94": {"label" : "minTemperatureToday", "type" : "float", "unit": "°F, 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": "°F, 210=NaN"},
"c911": {"label" : "maxHeatIndexToday", "type" : "float", "unit": "°F, 210=NaN"},
"c912": {"label" : "minHeatIndexToday", "type" : "float", "unit": "°F, 210=NaN"},
"c913": {"label" : "dewPointTemperature", "type" : "float", "unit": "°F, 210=NaN"},
"c914": {"label" : "maxDewPointTemperatureToday", "type" : "float", "unit": "°F"},
"c915": {"label" : "minDewPointTemperatureToday", "type" : "float", "unit": "°F"}
},
"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": "°F, 210=NaN"},
"c32": {"label" : "temperature_trend", "type" : "int", "unit": "0=steady, 1=rise, 2=fall"},
"c33": {"label" : "temperature_max", "type" : "float", "unit": "°F, 210=NaN"},
"c34": {"label" : "temperature_min", "type" : "float", "unit": "°F, 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": "°F, 210=NaN"},
"c311": {"label" : "heat_index_max", "type" : "float", "unit": "°F, 210=NaN"},
"c312": {"label" : "heat_index_min", "type" : "float", "unit": "°F, 210=NaN"},
"c313": {"label" : "dew_point_temperature", "type" : "float", "unit": "°F, 210=NaN"},
"c314": {"label" : "dew_point_max", "type" : "float", "unit": "°F"},
"c315": {"label" : "dew_point_min", "type" : "float", "unit": "°F"}
},
"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": "°F, 210=NaN"},
"c27": {"label" : "today_min_wind_chill", "type" : "float", "unit": "°F, 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 {"time":"2022-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 also 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 update each time the server setup change, which may not be feasible, or
- 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 J-Link or other OpenOCD-compatible tools.
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, the function calls used for enabling TLS are identified and patched - additionally, the HTTP port can be changed from the default
443
.
To reproduce the complete workspace setup, see following chapter.
For a WMR500 that reports the firmware version as1490
, the following binary changes are to be made:- 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
, - (OPTIONAL) Immediate value of Move Top instruction (
MOVW
) at address0x0801b630
, responsible for loading the port number, to be replaced with the desired value (1
to65535
decimal).
- Branch instruction (
- After modifying the firmware, flashing it back on the WMR500 will enable the changes.
Note: To obtain firmware version number, either:- check value of key
c82
in the response obtained when requesting the measurement values), - hold
select
andup
buttons on the WMR500 for two seconds.
A fully-patched firmware image, with a new HTTP port of 50007, is present in this repo. A non-modified firmware dump is also included.
- check value of key
The following steps are applicable only for a patched WMR500.
- Install the required python libraries:
sudo pip install Flask gunicorn
(why gunicorn?). - Optionally, edit the
http_wmr500.py
file by configuring the HTTP port (HTTP_PORT
) patched on the WMR500 (default 50007). - Run the Python script as root:
sudo gunicorn http_wmr500:app -b 0.0.0.0:xxxx
, wherexxxx
is the HTTP port.
- 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
(seconds: /30
means every 30 seconds, for 1 minute useminutes: /1
), and_GUUID_
(WMR500's GUUID).
- id: '_AUTOMATION_ID_'
alias: WMR500_Update_Trigger
description: ''
trigger:
- platform: time_pattern
seconds: /30
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 five times the sample period (as defined by the automation trigger period), for eg. 300.
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
unit_of_measurement: "°F"
expire_after: 300
- 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
unit_of_measurement: "%"
expire_after: 300
- 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
unit_of_measurement: "°F"
expire_after: 300
- 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
unit_of_measurement: "%"
expire_after: 300
- 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)}}"
icon: "mdi:wind-turbine"
unit_of_measurement: "km/h"
expire_after: 300
- name: OUTDOOR_RAIN
unique_id: "wmr500_outdoor_rain"
state_topic: "enno/in/json"
value_template: "{{ value_json['data']['6']['outdoor']['channel1']['w4']['c41'] }}"
icon: "mdi:weather-pouring"
unit_of_measurement: "mm"
expire_after: 300
- 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
unit_of_measurement: "hPa"
expire_after: 300
- 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 with 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.1.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 previous chapter. - 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 example is a hint given by string at address0x80051ad8
-Starting WICED v3.5.2
, which mentions the library used for network protocols - 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
/ML_RX
pins.
All the reverse-engineering, development, integration, and documentation efforts are based on the latest software and hardware versions available at the time of writing (September 2022), and licensed under the GNU General Public License v3.0.