From a3543aaec320e56c6f8a7c419707ac1aa40c4408 Mon Sep 17 00:00:00 2001
From: Someone <someone@somenet.org>
Date: Sat, 29 May 2021 18:44:01 +0200
Subject: [PATCH] Periodically fetch wh1080 data.

---
 cron.sh           |  40 +++++++++++++
 fetch_data.py     | 146 ++++++++++++++++++++++++++++++++++++++++++++++
 requirements.txt  |   1 +
 wh1080.cron       |   4 ++
 wh1080.udev-rules |   4 ++
 5 files changed, 195 insertions(+)
 create mode 100755 cron.sh
 create mode 100755 fetch_data.py
 create mode 100644 requirements.txt
 create mode 100644 wh1080.cron
 create mode 100644 wh1080.udev-rules

diff --git a/cron.sh b/cron.sh
new file mode 100755
index 0000000..ca2e700
--- /dev/null
+++ b/cron.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+# This script is run every min and does all the data gathering.
+
+MYPWD=$(pwd)
+
+mkdir -p /tmp/wh1080/device
+START_TS="$(date -Isec)"
+echo "** cron.sh: started: ${START_TS}"
+echo "${START_TS}" >/tmp/wh1080/cron.sh.start.ts
+echo "${START_TS}" >/tmp/wh1080/device/last.ts
+
+
+
+# cleanup old history data.
+cd /tmp/wh1080/device
+find . -type f -mmin +1440 -delete >/dev/null 2>&1
+find . -empty -delete >/dev/null 2>&1
+cd /tmp/wh1080
+
+
+# get data
+echo "** cron.sh: processing"
+mkdir -p /tmp/wh1080/device/history
+cd "$MYPWD"
+./fetch_data.py >/tmp/wh1080/device/last.json.new
+
+mv /tmp/wh1080/device/last.json.new /tmp/wh1080/device/last.json
+cat /tmp/wh1080/device/last.json >"/tmp/wh1080/device/history/data.$(cat /tmp/wh1080/device/last.ts).json"
+cd /tmp/wh1080/device/history
+echo "[$(ls --format=commas -Q)]" >/tmp/wh1080/device/history.json
+
+
+
+# finish
+ln -snf /tmp/wh1080.cron.sh.log /tmp/wh1080/cron.sh.log
+
+END_TS="$(date -Isec)"
+echo "${END_TS}" >/tmp/wh1080/cron.sh.stop.ts
+echo "** cron.sh: DONE: ${END_TS}"
diff --git a/fetch_data.py b/fetch_data.py
new file mode 100755
index 0000000..0e3d591
--- /dev/null
+++ b/fetch_data.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python3
+# Model Dreamlink WH1080
+#
+
+import usb.core
+import time
+import struct
+import math
+import datetime
+
+VENDOR = 0x1941
+PRODUCT = 0x8021
+WIND_DIRS = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']
+
+
+def open_ws():
+    '''
+    Open a connection to the device, using the PRODUCT and VENDOR information
+    @return reference to the device
+    '''
+    usb_device = usb.core.find(idVendor=VENDOR, idProduct=PRODUCT)
+
+    if usb_device is None:
+        raise ValueError('Device not found')
+
+    usb_device.get_active_configuration()
+
+    # If we don't detach the kernel driver we get I/O errors
+    if usb_device.is_kernel_driver_active(0):
+        usb_device.detach_kernel_driver(0)
+
+    return usb_device
+
+
+def read_block(device, offset):
+    '''
+    Read a block of data from the specified device, starting at the given offset.
+    @Inputs
+    device
+        - usb_device
+    offset
+        - int value
+    @Return byte array
+    '''
+
+    least_significant_bit = offset & 0xFF
+    most_significant_bit = offset >> 8 & 0xFF
+
+    # Construct a binary message
+    tbuf = struct.pack('BBBBBBBB',
+                       0xA1,
+                       most_significant_bit,
+                       least_significant_bit,
+                       32,
+                       0xA1,
+                       most_significant_bit,
+                       least_significant_bit,
+                       32)
+
+    timeout = 1000  # Milliseconds
+    retval = device.ctrl_transfer(0x21,  # USB Requesttype
+                               0x09,  # USB Request
+                               0x200,  # Value
+                               0,  # Index
+                               tbuf,  # Message
+                               timeout)
+
+    return device.read(0x81, 32, timeout)
+
+
+def main():
+    dev = open_ws()
+    dev.set_configuration()
+
+
+    ########### Read data
+    # Get the first 32 Bytes of the fixed
+    fixed_block = read_block(dev, 0)
+
+    # Check that we have good data
+    if (fixed_block[0] != 0x55):
+        raise ValueError('Bad data returned')
+
+    # Bytes 31 and 32 when combined create an unsigned short int that tells us where to find the weather data we want
+    curpos = struct.unpack('H', fixed_block[30:32])[0]
+    current_block = read_block(dev, curpos)
+
+    # Indoor information
+    indoor_humidity = current_block[1]
+    tlsb = current_block[2]
+    tmsb = current_block[3] & 0x7f
+    tsign = current_block[3] >> 7
+    indoor_temperature = (tmsb * 256 + tlsb) * 0.1
+    # Check if temperature is less than zero
+    if tsign:
+        indoor_temperature *= -1
+
+    # Outdoor information
+    outdoor_humidity = current_block[4]
+    tlsb = current_block[5]
+    tmsb = current_block[6] & 0x7f
+    tsign = current_block[6] >> 7
+    outdoor_temperature = (tmsb * 256 + tlsb) * 0.1
+    # Check if temperature is less than zero
+    if tsign:
+        outdoor_temperature *= -1
+
+    # Bytes 8 and 9 when combined create an unsigned short int that we multiply by 0.1 to find the absolute pressure
+    abs_pressure = struct.unpack('H', current_block[7:9])[0]*0.1
+
+    wind = current_block[9]
+    gust = current_block[10]
+    wind_extra = current_block[11]
+    wind_dir = current_block[12]
+
+    # Bytes 14 and 15  when combined create an unsigned short int
+    # that we multiply by 0.3 to find the total rain
+    # I'm not confident that this is correct. Neither abs_pressure nor
+    # total_rain are returning sane values. In fact total_rain has
+    # stayed static despite rainfall
+    # Looks like I fixed it. They used fixed_block instead of current_block
+    total_rain = struct.unpack('H', current_block[13:15])[0]*0.3
+
+    # Calculate wind speeds
+    wind_speed = (wind + ((wind_extra & 0x0F) << 8)) * 0.38  # Was 0.1
+    gust_speed = (gust + ((wind_extra & 0xF0) << 4)) * 0.38  # Was 0.1
+
+
+    ############### print data
+    print('{')
+    print('"indoor_humidity": "%i",' %indoor_humidity)
+    print('"indoor_temperature": "%2.1f",' %indoor_temperature)
+    print('"outdoor_humidity": "%i",' %outdoor_humidity)
+    print('"outdoor_temperature": "%2.1f",' %outdoor_temperature)
+    print('"abs_pressure": "%4.1f",' %abs_pressure)
+    print('"total_rain": "%3.1f",' %total_rain)
+    print('"wind_speed": "%2.1f",' %wind_speed)
+    print('"gust_speed": "%2.1f",' %gust_speed)
+    if wind_dir != 128:
+        print('"wind_dir": "%s",' %WIND_DIRS[wind_dir])
+    print('"TS": "%s"' %(datetime.datetime.now().isoformat()))
+    print('}')
+
+
+if __name__ == "__main__":
+    main()
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..6513d5e
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1 @@
+pyusb
diff --git a/wh1080.cron b/wh1080.cron
new file mode 100644
index 0000000..c7cc48f
--- /dev/null
+++ b/wh1080.cron
@@ -0,0 +1,4 @@
+# cp wh1080.cron /etc/cron.d/wh1080
+
+# fetch data from WH1080
+* * * * * root (cd /root/gitstuff/pyWH1080; ./cron.sh) &>/tmp/wh1080.cron.sh.log
diff --git a/wh1080.udev-rules b/wh1080.udev-rules
new file mode 100644
index 0000000..2da17a3
--- /dev/null
+++ b/wh1080.udev-rules
@@ -0,0 +1,4 @@
+#
+# cp udev-90-wh1080.rules /etc/udev/rules.d/90-wh1080.rules
+
+SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="1941", ATTR{idProduct}=="8021", MODE="0660", GROUP="plugdev"
-- 
2.43.0