272 lines
7.8 KiB
TypeScript
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;
|