node-ws1080-usb/src/ws1080.ts

272 lines
7.8 KiB
TypeScript

import { getDeviceList, Device, Interface, InEndpoint } from 'usb';
const MAX_RAIN_JUMP = 10;
const WIND_DIRS = [
'N',
'NNE',
'NE',
'ENE',
'E',
'ESE',
'SE',
'SSE',
'S',
'SSW',
'SW',
'WSW',
'W',
'WNW',
'NW',
'NNW',
];
/**
* Model Dreamlink WH1080/WS1080
*
* This is a Node.js port of
* https://github.com/shaneHowearth/Dream-Link-WH1080-Weather-Station
*
* If running Ubuntu and getting permissions errors,
* follow the advice given here http://ubuntuforums.org/showthread.php?t=901891
*
* I found the GROUPS portion was REQUIRED on one machine, set it to an appropriate group for
* your system.
*
* `/etc/udev/rules.d/41-weather-device.rules`
*
* `SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device",SYSFS{idVendor}=="1941" , SYSFS{idProduct}=="8021", MODE="0666", GROUPS="shane"`
*
* OTOH my second machine requires these rules:
* `SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device",ATTR{idVendor}=="1941" , ATTR{idProduct}=="8021", MODE="0666", OWNER="shane"`
*
* Run `sudo udevadm control --reload-rules && sudo udevadm trigger` to reload the rules.
*
* Then remove your device from your machine and plug it back in again
*/
class WS1080 {
private previousRain = 0;
constructor(private device: Device, private dInterface: Interface) {}
/**
* Open USB device.
* @param vendor Vendor ID
* @param product Product ID
* @returns USB device and interface
*/
static fromDevice(vendor = 0x1941, product = 0x8021) {
const devices = getDeviceList();
const device = devices.find(
(dev) =>
dev.deviceDescriptor.idVendor === vendor &&
dev.deviceDescriptor.idProduct === product
);
if (!device) {
throw new Error('Device not found');
}
device.open();
const iface = device.interface(0);
// If we don't detach the kernel driver we get I/O errors
if (iface.isKernelDriverActive()) {
iface.detachKernelDriver();
}
iface.claim();
device.timeout = 500;
return new WS1080(device, iface);
}
/**
* Read a block of data from the specified device, starting at the given offset.
* @param device USB
* @param iface USB Interface
* @param offset Byte offset
* @returns Buffer
*/
static async readBlock(device: Device, iface: Interface, offset: number) {
const least_significant_bit = offset & 0xff;
const most_significant_bit = (offset >> 8) & 0xff;
// Construct a binary message
const buffer = Buffer.from([
0xa1,
most_significant_bit,
least_significant_bit,
32,
0xa1,
most_significant_bit,
least_significant_bit,
32,
]);
// Send the binary message to the device via control transfer
await new Promise<number | Buffer | undefined>((resolve, reject) =>
device.controlTransfer(0x21, 0x09, 0x200, 0, buffer, (err, buf) =>
err ? reject(err) : resolve(buf)
)
);
// Read the response from the IN endpoint
return new Promise<Buffer | undefined>((resolve, reject) => {
const endpoint = iface.endpoints.find(
(endpoint) => endpoint.address === 0x81
) as InEndpoint;
if (!endpoint) return reject('Correct IN endpoint was not found');
endpoint.transfer(32, (err, buf) => (err ? reject(err) : resolve(buf)));
});
}
/**
* Using the supplied temperature and humidity calculate the dew point
* From Wikipedia: The dew point is the temperature at which the water vapor
* in a sample of air at constant barometric pressure condenses into liquid
* water at the same rate at which it evaporates. At temperatures below
* the dew point, water will leave the air. The condensed water is called dew
* when it forms on a solid surface. The condensed water is called either fog
* or a cloud, depending on its altitude, when it forms in the air.
* @param temperature - Temperature
* @param humidity - Humidity
* @returns dew point
*/
static dewPoint(temperature: number, humidity: number) {
humidity /= 100.0;
const gamma =
(17.271 * temperature) / (237.7 + temperature) + Math.log(humidity);
return (237.7 * gamma) / (17.271 - gamma);
}
/**
* Return wind chill temp based on temperature & wind speed.
*
* Using the supplied temperature and wind speed calculate the wind chill
* factor.
*
* From Wikipedia: Wind-chill or windchill, (popularly wind chill factor) is
* the perceived decrease in air temperature felt by the body on exposed skin
* due to the flow of air.
*
* @param temperature Temperature
* @param wind Wind speed
* @returns Wind chill temperature
*/
static windChill(temperature: number, wind: number) {
const wind_kph = 3.6 * wind;
// Low wind speed, or high temperature, negates any perceived wind chill
if (wind_kph <= 4.8 || temperature > 10.0) return temperature;
const wct =
13.12 +
0.6215 * temperature -
11.37 * wind_kph ** 0.16 +
0.3965 * temperature * wind_kph ** 0.16;
// Return the lower of temperature or wind chill temperature
if (wct < temperature) return wct;
else return temperature;
}
/**
* Read weather data from WS1080
* @returns Weather data
*/
async read() {
const block = await WS1080.readBlock(this.device, this.dInterface, 0);
if (!block) throw new Error('No data returned for request.');
if (block[0] !== 0x55)
throw new Error('Invalid data returned for request.');
const offset = block.subarray(30, 32).readUint16LE();
const currentBlock = await WS1080.readBlock(
this.device,
this.dInterface,
offset
);
if (!currentBlock) throw new Error('No data returned for request.');
// Indoor information
const indoorHumidity = currentBlock[1];
let tlsb = currentBlock[2];
let tmsb = currentBlock[3] & 0x7f;
let tsign = currentBlock[3] >> 7;
let indoorTemperature = (tmsb * 256 + tlsb) * 0.1;
// Check if temperature is less than zero
if (tsign) indoorTemperature *= -1;
// Outdoor information
const outdoorHumidity = currentBlock[4];
tlsb = currentBlock[5];
tmsb = currentBlock[6] & 0x7f;
tsign = currentBlock[6] >> 7;
let outdoorTemperature = (tmsb * 256 + tlsb) * 0.1;
// Check if temperature is less than zero
if (tsign) outdoorTemperature *= -1;
// Bytes 8 and 9 when combined create an unsigned short int
// that we multiply by 0.1 to find the absolute pressure
const absPressure = currentBlock.subarray(7, 9).readUint16LE() * 0.1;
const wind = currentBlock[9];
const gust = currentBlock[10];
const windExtra = currentBlock[11];
const windDir = currentBlock[12];
const windDirection = WIND_DIRS[windDir];
// Bytes 14 and 15 when combined create an unsigned short int
// that we multiply by 0.3 to find the total rain
let totalRain = currentBlock.subarray(13, 15).readUint16LE() * 0.3;
// Calculate wind speeds
const windSpeed = (wind + ((windExtra & 0x0f) << 8)) * 0.38; // Was 0.1
const gustSpeed = (gust + ((windExtra & 0xf0) << 4)) * 0.38; // Was 0.1
const outdoorDewPoint = WS1080.dewPoint(
outdoorTemperature,
outdoorHumidity
);
const windChillTemp = WS1080.windChill(outdoorTemperature, windSpeed);
// Calculate rainfall rates
if (this.previousRain === 0) this.previousRain = totalRain;
let rainDiff = totalRain - this.previousRain;
if (rainDiff > MAX_RAIN_JUMP) {
// Filter rainfall spikes
rainDiff = 0;
totalRain = this.previousRain;
}
this.previousRain = totalRain;
// Return the data! Finally!
return {
indoorHumidity,
outdoorHumidity,
indoorTemperature,
outdoorTemperature,
outdoorDewPoint,
windChillTemp,
windSpeed,
gustSpeed,
windDirection,
rainDiff,
totalRain,
absPressure,
};
}
close() {
this.device.close();
}
}
export default WS1080;