Hacking the FT-0310 Weather Station

So the title may be a bit misleading, but hell, it might help you find what you’re looking for.

I recently replaced my 10-year old wired Arduino-based weather station (DHT22 + BMP085 + background radiation via SMB-20 geiger tube mounted in a wooden screen in the garden) with a spanking new NicetyMeter 0310 Professional WiFi Weather Station which, in spite of its daft brand name, turned out to be a fine purchase.

The Hardware

The weather station consists of a Wifi-enabled display console (3xAAA powered, with an optional power adapter which turns out not to be at all optional if you want to use WIFI logging) and an integrated outdoor sensor which looks suspiciously like a Davis Vantage Vue (photos below taken from the seller’s website) with an UV and light sensor. It comes with a full mounting kit and the main difference between it and its ultra-expensive ‘inspiration’ is the fact that this sensor doesn’t charge the backup batteries (i.e. the solar panel is only used to power the transmitter during the daytime, 3xAA batteries power it during the night) so batteries need to be replaced occasionally, the manufacturer claims they should last more than a year.

A casual google search shows that this particular combination of display console and sensor is sold under several brands (including Sainlogic WS 0310 and Ragova) while the outdoor sensor unit is used by even more brands (more on that later). The manufacturer seems to be StarMeter Instruments.

For what it’s worth, it’s a good value for money. The housing seems to made from solid ABS and doesn’t feel flimsy or cheap.  One issue I have with the external console is the plastic mounting pole that tends to wobble slightly, activating the rain sensor tipping cups during heavy wind gusts, but I assume this is partially caused by my choice of mounting location, which is on a 1 metre high antenna pole atop a ladder mounted on the side of my house.

Getting its data

In addition to displaying the data, the display console can upload it to Weather Underground and Weathercloud. The user manual is written well (it’s not Chinglish), so it’s easy to set up logging and for most most people this would be enough, but I wouldn’t be me if I just kept it at that. Both services have their limitations – Weathercloud only allows 10-minute updates and access to only a year’s worth of data without a subscription, while Wunderground accepts 1-minute updates (the station console doesn’t support rapidfire updates) but the free API key you get if you register a weather station is limited to 1500 queries PER DAY, which means you’re only allowed to query their servers once a minute.

My priority was to back up my station’s observations (and post them to other weather observation websites) as best as I could until I find a different solution, so I’ve set up leoherzog‘s brilliantly simple WundergroundStationForwarder Google script to work with my station’s data and publish it to some additional backends. It’s a hacky solution, but seems to work pretty reliably. Once I got that sorted, it was time to look under the hood. After all, the sensor transmits every 16 seconds, and yet I get online data every 60 seconds. I won’t have any of that nonsense!

The Teardown

Since the probability that the display console had an ESP8266 or something similar inside was reasonably high (the Soft AP configuration and the WeatherHome-CFxxxxx were dead giveaways), I hoped I would be able to find a debug port and use that to gain access to data.

It turned out that I was right-ish. The console’s WiFi connectivity is powered by an ESP-WROOM-02D-V1.4, but its sole purposes seem to be web logging and NTP time sync. The four pin holes (P5 header) next to it are, top to bottom, VCC, GND, TXD, RXD.

The UART0 port has no useful debugging information apart from the typical boot debug, so I assume it’s intended for flashing. A boot log looks like this:

ets Jan  8 2013,rst cause:1, boot mode:(7,7)

waiting for host
ets Jan  8 2013,rst cause:1, boot mode:(7,7)

waiting for host

ets Jan  8 2013,rst cause:2, boot mode:(3,7)

load 0x40100000, len 2592, room 16 
tail 0
chksum 0xf3
load 0x3ffe8000, len 764, room 8 
tail 4
chksum 0x92
load 0x3ffe82fc, len 676, room 4 
tail 0
chksum 0x22
csum 0x22

2nd boot version : 1.7(5d6f877)
SPI Speed : 40MHz
SPI Mode : QIO
SPI Flash Size & Map: 16Mbit(1024KB+1024KB)
jump to run user1 @ 1000

OS SDK ver: 2.0.0(e271380) compiled @ Mar 30 2018 18:54:06
phy ver: 1055_1, pp ver: 10.7

rf cal sector: 507
tcpip_task_hdl : 40107b28, prio:10,stack:512
idle_task_hdl : 40107bd8,prio:0, stack:384
tim_task_hdl : 40107d20, prio:2,stack:512
Load param success
Timer Server: time.windows.com
20000128128128000001281280
 ets Jan  8 2013,rst cause:2, boot mode:(3,6)

load 0x40100000, len 2592, room 16 
tail 0
chksum 0xf3
load 0x3ffe8000, len 764, room 8 
tail 4
chksum 0x92
load 0x3ffe82fc, len 676, room 4 
tail 0
chksum 0x22
csum 0x22

2nd boot version : 1.7(5d6f877)
SPI Speed : 40MHz
SPI Mode : QIO
SPI Flash Size & Map: 16Mbit(1024KB+1024KB)
jump to run user1 @ 1000

OS SDK ver: 2.0.0(e271380) compiled @ Mar 30 2018 18:54:06
phy ver: 1055_1, pp ver: 10.7

rf cal sector: 507
tcpip_task_hdl : 40107b28, prio:10,stack:512
idle_task_hdl : 40107bd8,prio:0, stack:384
tim_task_hdl : 40107d20, prio:2,stack:512
Load param success
Timer Server: time.windows.com
20000128128128000001281280
Mainboard, bottom view

The board also has two microprocessors (custom chip-on-board, covered in epoxy resin) which run the LCD display and everything else.

“Everything else” includes:

  • A two-wire I2C EEPROM and a barometric pressure sensor. There’s a pin breakout for the EEPROM.
  • A 433 MHz receiver with its own pin breakout
  • A humidity sensor

Since the ESP is connected to the main processor(s) via IO 12, IO 13, IO 14 and IO 15, it is evident that it’s using the SPI protocol, but I figured it would be too time-consuming to reverse-engineer the communication and since the processor only talks to the ESP module when a web update is required (i.e. once a minute), I realised that I will just have to get the data directly from the outdoor transmitter.

Sniffing data from the outdoor sensor

The next step was to break out my cheapo RTL2832U-based SDR receiver (25 MHz to 1760 MHz version, yes, the blue 10$ one) and see what it can find with the aid of RTL_433 decoder. It did not disappoint. The latest version of RTL_433 identified it as Cotech-367959 (or other stations that use the same sensor, SwitchDoc Labs Weather FT020T,  Sainlogic Weather Station WS019T and probably many more).

RTL_433 identifies the station as Cotech-36759

Please note that at the time of writing only the latest version supports it fully and properly decodes UV index and light intensity, the Debian package (installed via apt install rtl-433) did not have UV index and Light Intensity, so I had to build it for my Orange Pi Zero running Armbian.

Using the data

This meant I could easily get the weather info and use it in WeeWX via the weewx-sdr driver. However, the driver doesn’t currently include support for Cotech-367959, so you need to add this class into the driver’s sdr.py. It’s usually located under weewx/user/sdr.py after installation.

class Cotech367959Packet(Packet):
    # ['{"time" : "2022-04-28 14:25:59", "model" : "Cotech-367959", "id" : 109, "battery_ok" : 1, "temperature_F" : 63.000, "humidity" : 55, "rain_mm" : 16.200, "wind_dir_deg" : 296, "wind_avg_m_s" : 0.900, "wind_max_m_s" : 1.300, "light_lux" : 23093, "uv" : 14, "mic" : "CRC"}']
    IDENTIFIER = "Cotech-367959"

    @staticmethod
    def parse_json(obj):
        pkt = dict()
        pkt['dateTime'] = Packet.parse_time(obj.get('time'))
        pkt['usUnits'] = weewx.METRIC
        sensor_id = obj.get('id') # changes every time you reset the outdoor sensor
        pkt['battery'] = 0 if obj.get('battery') == 'OK' else 1
        pkt['temperature'] = to_C(Packet.get_float(obj, 'temperature_F'))
        pkt['humidity'] = Packet.get_float(obj, 'humidity')
        pkt['wind_gust'] = Packet.get_float(obj, 'wind_max_m_s')
        pkt['wind_speed'] = Packet.get_float(obj, 'wind_avg_m_s')
        pkt['wind_dir'] = Packet.get_float(obj, 'wind_dir_deg')
        pkt['total_rain'] = Packet.get_float(obj, 'rain_mm')
        pkt['light_lux'] = Packet.get_int(obj, 'light_lux')
        pkt['uv'] = Packet.get_int(obj, 'uv') / 10
        pkt = Packet.add_identifiers(pkt, sensor_id, Cotech367959Packet.__name__)
        return pkt

Also bear in mind that the id value changes whenever you reset the outdoor sensor, so if you’ve correctly identified your station and plan on using it continuously, you might want to set a fixed value there.

As noted in the comments below, the original sdr.py doesn’t provide a function that converts degrees Fahrenheit to Celsius, so you should also add one – just insert the following (utility functions start from line 169).

def to_C(v):
    if v is not None:
        v  = 5 / 9 * (v - 32)
    return v

To use this data in WeeWX, you also need to map the values in /etc/weewx/weewx.conf:

[SDR]
    # This section is for the software-defined radio driver.
    # The driver to use
    driver = user.sdr

    [[sensor_map]]
        windDir = wind_dir.109.Cotech367959Packet
        windSpeed = wind_speed.109.Cotech367959Packet
        windGust = wind_gust.109.Cotech367959Packet
        outTemp = temperature.109.Cotech367959Packet
        outHumidity = humidity.109.Cotech367959Packet
        rain_total = total_rain.109.Cotech367959Packet
        UV = uv.109.Cotech367959Packet
        luminosity = light_lux.109.Cotech367959Packet

Note that the 109 is my station ID, yours will be different (unless you change it or set a fixed value, which I recommend, see above).

So now I had almost all the data I needed, apart from barometric pressure. For this purpose, I connected a BME180 breakout board to my OrangePi/Raspberry Pi using the I2C port. To get the data into WeeWX, I used this Bme280wx driver (not a typo, BME180/280 are interchangeble).

I had to combine the data – mapping the BME180 data to inHumidity, inTemp and pressure values in /etc/weewx/weewx.conf as shown below.

[Bme280wx]
i2c_port = 0 # OrangePi and older Raspberry Pi's use 0, usually it's 1
i2c_address = 0x76
usUnits = US
temperatureKeys = inTemp
temperature_must_have = ""
pressureKeys = pressure
pressure_must_have = outTemp
humidityKeys = inHumidity
humidity_must_have = ""

Additionally, you need to tell WeeWX to load this additional driver in the Services section of weewx.conf.

[Engine]

    # The following section specifies which services should be run and in what order.
    [[Services]]
        data_services = "", user.bme280wx.Bme280wx

station_type value in the [Station] section should be defined as SDR since RTL-SDR is your main driver and BME280wx your additional driver.

[Station]
    [...]

    # Set to type of station hardware. There must be a corresponding stanza
    # in this file, which includes a value for the 'driver' option.
    station_type = SDR

My Weewx with this data is accessible at https://meteo-valpovo.sh.com.hr/.

The FUTURE

Since I’m not too confident about my cheapo SDR receiver, my plan is to set up a dedicated ESP32 or ESP8266-based 433 MHz receiver and forwarder if I manage to implement it (or if someone smarter ports the RTL-433 project to a suitable library).

Having a local backup of my weather data and an alternative method of posting it to online services which doesn’t rely on my closed-source display console is enough for now, since I doubt I’d get a firmware upgrade if Weather Underground or WeatherCloud suddenly decided to change their API’s.

 

9 thoughts on “Hacking the FT-0310 Weather Station”

  1. Thanks for that. I am using Home Assistant with an add in that has rtl_433 in a container – Docker? – exposing sensors via MQTT. That’s an interesting debate on Github and looks like the code will be updated one way or another.

    1. Docker tends to complicate matters in this case, although I understand that it’s the easiest way to set up RTL_433 if you’re using homeassistant. If the RTL_433 docker container is built from the latest github sources or the last release version (21.12), both UV (scaled by 10) and light_lux (capped at 69627) should be there, there’s no need to map any values.

  2. Hi. Tried with a similar station, getting an error on weewx startup:
    CRITICAL main: Caught unrecoverable exception:
    Jul 16 13:36:27 packetpi weewx[2527] CRITICAL main: **** name ‘to_C’ is not defined
    Jul 16 13:36:27 packetpi weewx[2527] CRITICAL main: **** Traceback (most recent call last):
    Jul 16 13:36:27 packetpi weewx[2527] CRITICAL main: **** File “/usr/share/weewx/weewxd”, line 153, in main
    Jul 16 13:36:27 packetpi weewx[2527] CRITICAL main: **** engine.run()
    Jul 16 13:36:27 packetpi weewx[2527] CRITICAL main: **** File “/usr/share/weewx/weewx/engine.py”, line 208, in run
    Jul 16 13:36:27 packetpi weewx[2527] CRITICAL main: **** for packet in self.console.genLoopPackets():
    Jul 16 13:36:27 packetpi weewx[2527] CRITICAL main: **** File “/usr/share/weewx/user/sdr.py”, line 3320, in genLoopPackets
    Jul 16 13:36:27 packetpi weewx[2527] CRITICAL main: **** for packet in PacketFactory.create(lines):
    Jul 16 13:36:27 packetpi weewx[2527] CRITICAL main: **** File “/usr/share/weewx/user/sdr.py”, line 3175, in create
    Jul 16 13:36:27 packetpi weewx[2527] CRITICAL main: **** pkt = PacketFactory.parse_json(lines)
    Jul 16 13:36:27 packetpi weewx[2527] CRITICAL main: **** File “/usr/share/weewx/user/sdr.py”, line 3191, in parse_json
    Jul 16 13:36:27 packetpi weewx[2527] CRITICAL main: **** return parser.parse_json(obj)
    Jul 16 13:36:27 packetpi weewx[2527] CRITICAL main: **** File “/usr/share/weewx/user/sdr.py”, line 1187, in parse_json
    Jul 16 13:36:27 packetpi weewx[2527] CRITICAL main: **** pkt[‘temperature’] = to_C(Packet.get_float(obj, ‘temperature_F’))
    Jul 16 13:36:27 packetpi weewx[2527] CRITICAL main: **** NameError: name ‘to_C’ is not defined
    Jul 16 13:36:27 packetpi weewx[2527] CRITICAL main: **** Exiting.

    Any ideia why?

    1. Yeah, you’re missing the to_C function which converts rtl_433’s and the weather station’s (native) Fahrenheit temperature value to degrees Celsius.
      You can do one of two things:
      1) add the function among the utility functions at the top of sdr.py, for example:

      def to_F(v):
          if v is not None:
              v  = v * 1.8 + 32
          return v
      
      def to_C(v):
          if v is not None:
              v  = 5 / 9 * (v - 32)
          return v

      or 2) Omit the conversion in sdr.py and let rtl_433 do that F to C conversion for you. To do this, change
      pkt['temperature'] = to_C(Packet.get_float(obj, 'temperature_F'))
      to
      pkt['temperature'] = Packet.get_float(obj, 'temperature_F')
      and change DEFAULT_CMD in sdr.py from
      DEFAULT_CMD = '/usr/local/bin/rtl_433 -M utc -F json'
      to
      DEFAULT_CMD = '/usr/local/bin/rtl_433 -M utc -F json -C si'

      Of course, there is no need to do both 🙂

      1. Thank you, Ivan. First method worked fine. Secondo also worked, but was missing outside temperature, so changed back to first.
        Hope for some further developments on the driver, there are a lot of these stations out there.

        Paulo

        1. Truth be told, I haven’t tested out the second method out so it’s purely theoretical. I might have missed something. Thanks for getting in touch, I’ve amended the original post to include the missing function.
          I should contact the author of the driver with this issue, but first we have to resolve the rtl_433 lux value issue before we proceed.

  3. Thanks for writing all this up, I’m going to give it a whirl as well for the 0310 I just ordered. I thought I’d be able to use this ecowitt integration in home assistant to connect it but realize I overjudged what was possible. I wish they would have just given a option to direct the data to a different source instead in the firmware but what can you do.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.