Source code for socs.agents.vantagepro2.drivers

import array as arr
import struct
import time

import numpy as np
from serial import Serial

# some commands require a CRC code (cyclic redundancy check) -
# these require the provided CRC table
crc_table = arr.array('H', [
    0x0, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
    0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
    0x1231, 0x210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
    0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
    0x2462, 0x3443, 0x420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
    0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
    0x3653, 0x2672, 0x1611, 0x630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
    0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
    0x48c4, 0x58e5, 0x6886, 0x78a7, 0x840, 0x1861, 0x2802, 0x3823,
    0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
    0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0xa50, 0x3a33, 0x2a12,
    0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
    0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0xc60, 0x1c41,
    0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
    0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0xe70,
    0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
    0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
    0x1080, 0xa1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
    0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
    0x2b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
    0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
    0x34e2, 0x24c3, 0x14a0, 0x481, 0x7466, 0x6447, 0x5424, 0x4405,
    0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
    0x26d3, 0x36f2, 0x691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
    0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
    0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x8e1, 0x3882, 0x28a3,
    0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
    0x4a75, 0x5a54, 0x6a37, 0x7a16, 0xaf1, 0x1ad0, 0x2ab3, 0x3a92,
    0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
    0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0xcc1,
    0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
    0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0xed1, 0x1ef0
])


[docs] def calc_crc(data): """Calculates CRC.""" crc = 0 for i in data: crc = crc_table[(crc >> 8) ^ i] ^ (crc << 8) crc = crc % 65536 return crc
[docs] def F_to_C(temp): """Function to convert fahrenheit measurement to celsius""" return (temp - 32) * (5 / 9)
[docs] def wind_chill(temp, wind): """Function to calculate wind chill temperature. Only valid if temp < 50F. Taken from https://www.calculator.net/wind-chill-calculator.html If temp > 50F, need to use heat index instead... Temp: Temperature in Fahrenheit wind: Speed in miles per hour """ # Calculation not valid above 50 F if temp > 50: return temp chill = 35.75 + 0.6215 * temp - 35.75 * np.power(wind, 0.16) + 0.4275 * temp * np.power(wind, 0.16) return chill
[docs] class VantagePro2: """Allows communication to Vantage Pro 2 Weather Monitor Module. Contains commands to be issued and member variables that store collected data. """ def __init__(self, path, baud=19200, timeout=1): """Establish serial connection and initialize member variables.""" if not path: path = '/dev/ttyUSB0' self.com = Serial(port=path, baudrate=baud, timeout=timeout) self.startup()
[docs] def startup(self): """Wakeup vantage pro 2 console.""" self.com.write(b"\n") for i in range(0, 3): response = self.com.read(2) if response == b'\n\r': break time.sleep(1.2) else: raise TimeoutError("Vantage Pro 2 console not woke")
[docs] def crc_check(self, data): """Checks received data. If received data has a CRC value of 0, then correct message received! """ crc = calc_crc(data) if crc != 0: print('Failed CRC. Errors in data received')
[docs] def close(self): self.com.close()
# closing serial if agent goes out of scope def __exit__(self): self.com.close()
[docs] def conditions_screen(self): """Move console to conditions screen (where loop command can succesfully be sent.) """ self.startup() self.com.write(b'RXTEST\n') time.sleep(2) # reads response from console for i in range(0, 3): ok = self.com.read(6) if ok == b'\n\rOK\n\r': break else: raise TimeoutError("Command not acknowledged")
[docs] def msg(self, msg): """Send general command or query to module. If command received and acknowledged, continue else retry 2 more times. """ attempt = 0 while attempt < 3: self.startup() self.com.write(bytes(msg, 'ascii')) time.sleep(0.1) ack = 0 for i in range(0, 3): ack = self.com.read(1) if ack == b'\x06': return True attempt += 1 else: return False
[docs] def interrupt_daq(self): """Interrupts loop command...if sent before loop command finishes.""" self.startup() self.com.write(b'\n') for i in range(0, 3): response = self.com.read(2) if response == b'\n\r': print("Data Acquisition succesfully interrupted") break time.sleep(0.1) else: raise TimeoutError('Data Acquisition not interrupted')
[docs] def receive_data(self): """Reads weather data from console and returns loop_data{}. User should read Vantage Pro Serial Communication Reference Manual for the format of the loop data packet. Specifically for units of data! Available fields: * Barometer trend: the current 3 hour barometer trend. Possible values:: -60 = Falling rapidly -20 = Falling slowly 0 = Steady 20 = Rising slowly 60 = Rising rapidly 80 = No trend available Any other value = The VantagePro2 does not have the 3 hours of data needed to determine the barometer trend. * Barometer: Current barometer reading (Hg/1000) * Inside Temperature: Temperatue in Celsius (10th of a degree) * Inside Humidity: Relative humidity in percent * Outside Temperature: Temperature in Celsius (10th of a degree) * Wind Speed: Wind speed in miles per hour * 10 min average wind speed: 10 minute average wind speed in mph * Wind Direction: From 1-360 degrees. Possible values:: 0 = No wind direction data 90 = East 180 = South 270 = West 360 = North * Extra Temperatures: Temperature from up to 7 extra temperature stations. * Soil Temperatures: Four soil temperature sensors, in the same format as the extra temperatures format listed above. * Leaf Temperatures: Four leaf temperature sensors, in the same format as the extra temperatures format listed above. * Outside Humidity: Relativie humidity in percent * Extra Humidities: Realtive humidity in percent for 7 humidity stations * Rain Rate: Number of rain clicks (0.1in or 0.2mm) per hour * UV: "Unit is in UV index" * Solar Radiation: Units in watt/meter^2 * Storm Rain: Stored in 100th of an inch * Start Date of Current storm: Gives month, date, and year (offset by 2000) * Day Rain: Number of rain clicks (0.1in or 0.2mm)/hour in the past day * Month Rain: Number of rain clicks (0.1in or 0.2mm)/hour in the past month * Year Rain: Number of rain clicks (0.1in or 0.2mm)/hour in the past year * Day ET: 1000th of an inch * Month ET: 1000th of an inch * Year ET: 1000th of an inch * Soil Moistures: In centibar, supports 4 soil sensors * Leaf Wetnesses: Scale from 0-15. Supports 4 leaf sensors. Possible values:: 0 = Very dry 15 = Very wet * Inside Alarms: Currently active inside alarms * Rain Alarms: Currently active rain alarms * Outside Alarms: Currently active outside alarms * Outside Humidity Alarms: Currently active humidity alarms * Extra Temp/Hum Alarms: Currently active extra temperature/humidity alarms * Soil & Leaf Alarms: Currently active soil/leaf alarms * Console Battery Voltage: Voltage * Time of Sunrise: Time is stored as hour x 100 + min * Time of Sunset: Time is stored as hour x 100 + min """ info = b'' # Give device multiple chances to send its data for i in range(0, 3): info = self.com.read(99) if info: break else: time.sleep(0.5) else: raise TimeoutError("Timeout error: no data from LOOP command") loop_data = {} byte_data = struct.unpack('=5b3h1b1h2B1H23b1h1b9h25b1h2b2h2c1h', info) loop_data['bar_trend'] = byte_data[3] loop_data['packet_type'] = byte_data[4] loop_data['next_record'] = byte_data[5] loop_data['barometer'] = byte_data[6] / 1000.0 loop_data['temp_inside'] = F_to_C(byte_data[7] / 10.0) loop_data['humidity_inside'] = byte_data[8] loop_data['temp_outside'] = F_to_C(byte_data[9] / 10.0) loop_data['wind_speed'] = byte_data[10] loop_data['avg_wind_speed'] = byte_data[11] loop_data['wind_dir'] = byte_data[12] loop_data['extra_temp0'] = F_to_C(byte_data[13] - 90.0) loop_data['extra_temp1'] = F_to_C(byte_data[14] - 90.0) loop_data['extra_temp2'] = F_to_C(byte_data[15] - 90.0) loop_data['extra_temp3'] = F_to_C(byte_data[16] - 90.0) loop_data['extra_temp4'] = F_to_C(byte_data[17] - 90.0) loop_data['extra_temp5'] = F_to_C(byte_data[18] - 90.0) loop_data['extra_temp6'] = F_to_C(byte_data[19] - 90.0) loop_data['soil_temp0'] = F_to_C(byte_data[20] - 90.0) loop_data['soil_temp1'] = F_to_C(byte_data[21] - 90.0) loop_data['soil_temp2'] = F_to_C(byte_data[22] - 90.0) loop_data['soil_temp3'] = F_to_C(byte_data[23] - 90.0) loop_data['leaf_temp0'] = F_to_C(byte_data[24] - 90.0) loop_data['leaf_temp1'] = F_to_C(byte_data[25] - 90.0) loop_data['leaf_temp2'] = F_to_C(byte_data[26] - 90.0) loop_data['leaf_temp3'] = F_to_C(byte_data[27] - 90.0) loop_data['humidity_outside'] = byte_data[28] loop_data['extra_hum0'] = byte_data[29] loop_data['extra_hum1'] = byte_data[30] loop_data['extra_hum2'] = byte_data[31] loop_data['extra_hum3'] = byte_data[32] loop_data['extra_hum4'] = byte_data[33] loop_data['extra_hum5'] = byte_data[34] loop_data['extra_hum6'] = byte_data[35] loop_data['rain_rate'] = byte_data[36] loop_data['UV'] = byte_data[37] loop_data['solar_rad'] = byte_data[38] loop_data['storm_rain'] = byte_data[39] / 100.0 loop_data['storm_start'] = byte_data[40] loop_data['day_rain'] = byte_data[41] loop_data['month_rain'] = byte_data[42] loop_data['year_rain'] = byte_data[43] loop_data['day_ET'] = byte_data[44] / 1000.0 loop_data['month_ET'] = byte_data[45] / 100.0 loop_data['year_ET'] = byte_data[46] / 100.0 loop_data['soil_moisture0'] = byte_data[47] loop_data['soil_moisture1'] = byte_data[48] loop_data['soil_moisture2'] = byte_data[49] loop_data['soil_moisture3'] = byte_data[50] loop_data['leaf_wetness0'] = byte_data[51] loop_data['leaf_wetness1'] = byte_data[52] loop_data['leaf_wetness2'] = byte_data[53] loop_data['leaf_wetness3'] = byte_data[54] loop_data['inside_alarm0'] = byte_data[55] loop_data['inside_alarm1'] = byte_data[56] loop_data['rain_alarm'] = byte_data[57] loop_data['outside_alarm0'] = byte_data[58] loop_data['outside_alarm1'] = byte_data[59] loop_data['extra_temp_hum_alarm0'] = byte_data[60] loop_data['extra_temp_hum_alarm1'] = byte_data[61] loop_data['extra_temp_hum_alarm2'] = byte_data[62] loop_data['extra_temp_hum_alarm3'] = byte_data[63] loop_data['extra_temp_hum_alarm4'] = byte_data[64] loop_data['extra_temp_hum_alarm5'] = byte_data[65] loop_data['extra_temp_hum_alarm6'] = byte_data[66] loop_data['soil_leaf_alarm0'] = byte_data[67] loop_data['soil_leaf_alarm1'] = byte_data[68] loop_data['soil_leaf_alarm2'] = byte_data[69] loop_data['soil_leaf_alarm3'] = byte_data[70] loop_data['transmitter_battery_status'] = byte_data[71] loop_data['console_battery_voltage'] = ((byte_data[72] * 300) / 512) / 100.0 loop_data['forecast_icons'] = byte_data[73] loop_data['forecast_rule_num'] = byte_data[74] loop_data['time_sunrise'] = byte_data[75] loop_data['time_sunset'] = byte_data[76] # Add wind chill temperature to observation data temp = byte_data[9] / 10.0 wind_speed = byte_data[10] loop_data['wind_chill_temp'] = F_to_C(wind_chill(temp, wind_speed)) # Fix overflow uvi = loop_data['UV'] if uvi < 0: uvi += 2**8 loop_data['UV'] = uvi # CRC check, data must be sent byte by byte pure_data = struct.unpack('=99b', info) self.crc_check(pure_data) return loop_data
[docs] def weather_daq(self): """Issues "LOOP 1"(which samples weather data) command to weather station, and unpacks the data. """ # Startup and issue loop command self.startup() command = "LOOP 1\n" if not self.msg(command): print("message failed to be sent!") # collect data return self.receive_data()
[docs] def print_data(self, data): """Prints contents of data collected from LOOP <loops> command. Loop: loop# Field : Value format. """ print("**************") for i in data: print("{} : {}".format(i, data[i]))