import pyowm import json #from datetime import datetime import time as time from time import gmtime, strftime, sleep from ctypes import * from datetime import date import ftd2xx import ftd2xx._ftd2xx as _ftd2xx import math import sys import serial import os import atexit #Declare constants RS485_START = 0x96 RS485_STOP = 0xA9 RS485_ACK = 0xAA PANL_ADDR = 0x4A RASP_ADDR = 0x4C IDLE_STATE = 0 DST_ADDR_STATE = 1 SRC_ADDR_STATE = 2 DATA_STATE = 3 PROCESS_STATE = 4 CRC_STATE = 5 STOP_STATE = 6 DAILY_WEATHER_DATA = (c_ubyte)(0) WEEKDAY_DATA = (c_ubyte)(1) CURRENT_TIME_DATA = (c_ubyte)(2) HOURLY_WEATHER_DATA = (c_ubyte)(3) SERVER_STARTUP_DATA = (c_ubyte)(4) LOCATION_CONFIRM_DATA = (c_ubyte)(5) GREETING_CMD = "OK" SIZEOF_GRETTING_CMD = 2 DAILY_WEATHER_CMD = "DAILY" SIZEOF_DAILY_CMD = 5 HOURLY_WEATHER_CMD = "HOURLY" SIZEOF_HOURLY_CMD = 6 TIME_CMD = "TIME" SIZEOF_TIME_CMD = 4 LOCATION_CMD = "LOCATION" SIZEOF_LOCATION_CMD = 8 WEEKDAY_CMD = "WEEKDAY" SIZEOF_WEEKDAY_CMD = 7 LOCATION = 'Singapore' NO_ACTION = 0 SEND_DAILY_DATA = 1 SEND_HOURLY_DATA = 2 SEND_CURRENT_TIME = 3 SEND_WEEKDAY = 4 SEND_LOCATION = 5 SEND_LOCATION_CONFIRM = 6 MAX_ACK_FAILURES = 3 MAX_REGIONS = 4 REFETCH_TIMEOUT_S = 60 class DailyWeather_t(Structure): _fields_ = [ ("temp", c_ubyte), ("min_temp", c_ubyte), ("max_temp", c_ubyte), ("humid", c_ubyte), ("wind_int", c_ubyte), ("wind_frac",c_ubyte), ("rain_int", c_ubyte), ("rain_frac",c_ubyte), ("epoch", c_uint), ("icon", c_char_p), ("description", c_char_p) ] class HourlyWeather_t(Structure): _fields_ = [ ("hour", c_ubyte), ("temp", c_ubyte), ("humid", c_ubyte), ("wind_int", c_ubyte), ("wind_frac",c_ubyte), ("rain_int", c_ubyte), ("rain_frac",c_ubyte), ("reserve", c_ubyte), ("epoch", c_uint), ("icon", c_char_p), ("description", c_char_p) ] #Declare global vars Location = "Singapore,sg" Parse_State = IDLE_STATE Data_Len = 0 Rx_Len = 0 Waiting_Ack = 0 Rs485_Buffer = (c_char * 100)() #create a 100-byte buffer to store RS485 packets Daily_Weather = (DailyWeather_t * 20)() #support max 4 regions since each region needs 5 DailyWeather_t Hourly_Weather = (HourlyWeather_t * 20)() Current_Action = NO_ACTION Next_Weather_Day = (c_byte)(0) ComPort_List = list() Active_COM = 0 Regions = [] Region_Utc_Zone = [8,8,1,7] #should make it sent from PanL35 Region_Count = 0 Server_Startup= 0 Waiting_Ack_Failures = 0 Utc_Time = "" Epoch_Time = 0 Weather_Data_Available = 0 Is_Internet_Available = 0 #Log_File = open("/home/pi/weather_station_log.txt", 'w') #create global OWM object owm = pyowm.OWM('dbbbde4905b735cbf7c09835af71bdac') def DoExit(): DoLog("Python exits") #Log_File.close() return #register exit function atexit.register(DoExit) def DoLog(string): #Log_File.write(string + "\n") print string return def crc8(buf, len): crc=0 for index in range(len): b = buf[index] if isinstance(b, str): inbyte = ord(b) else: inbyte = int(b) #print inbyte for i in range(8): mix = (crc ^ inbyte) & 0x01 crc >>= 1 if mix: crc ^= 0x8C inbyte >>= 1 return crc def convert_hex(val): return hex(ord(val)) def convert_int(val): return ord(val) def Rs485_Send(data, len): buf_len = len + 6 buf = (c_ubyte * buf_len)() buf[0] = RS485_START buf[1] = PANL_ADDR buf[2] = RASP_ADDR buf[3] = len memmove(addressof(buf) + 4, byref(data), len) buf1 = (c_ubyte * buf_len)() memmove(buf1, addressof(buf) + 1, len+3) buf[4+len] = crc8(buf1, len+3) buf[-1] = RS485_STOP val = "" for i in buf: val += str(hex(i)) p_char = cast(buf, c_char_p) #print("Tx len:" + str(buf_len)) #print("Tx:" + val) try: Active_COM.write_binary(buf, len + 6) except serial.SerialException: Current_Action = NO_ACTION Waiting_Ack = 0 return def Sending_Ack(): data = (c_ubyte * 1)(RS485_ACK) Rs485_Send(data, 1) return def Execute_Command(data, data_len): global Regions global Region_Count next_action = NO_ACTION cmd = data[3:data_len] #print ("Cmd:" + str(cmd)) if cmd[:SIZEOF_GRETTING_CMD] == GREETING_CMD: DoLog("Receive Greeting from PanL") elif cmd[:SIZEOF_DAILY_CMD] == DAILY_WEATHER_CMD: next_action = SEND_DAILY_DATA elif cmd == HOURLY_WEATHER_CMD: next_action = SEND_HOURLY_DATA elif cmd == TIME_CMD: next_action = SEND_CURRENT_TIME elif cmd[:SIZEOF_LOCATION_CMD] == LOCATION_CMD: array = data[11:data_len].split('#') Regions = [] for element in array: Regions.append(element[:-1]) #Region_Utc_Zone.append(element[-1]) #del Regions[-1] #last element is a dummy value, so we delete it #for i in range(len(Regions)): # print("Region: " + Regions[i] + " Zone: " + str(Region_Utc_Zone[i])) next_action = SEND_LOCATION_CONFIRM elif cmd == WEEKDAY_CMD: next_action = SEND_WEEKDAY else: DoLog("Invalid command") return next_action def Take_Action(): global Waiting_Ack global Next_Weather_Day global Current_Action global Region_Count global Server_Startup global Waiting_Ack_Failures global Utc_Time global Is_First_Send global Weather_Data_Available if Waiting_Ack == 1 or Current_Action == NO_ACTION: #no action if waiting for ACK return tx_len = 0 if Current_Action == SEND_DAILY_DATA: #retrieve weather data for a region DoLog("Send data for region: " + Regions[Region_Count]) if Weather_Data_Available == 0: Fetch_Weather_Data() memmove(Rs485_Buffer, byref(DAILY_WEATHER_DATA),1) memmove(addressof(Rs485_Buffer) + 1, byref(c_ubyte(Region_Count)),1) tx_len = 2 start = 5 * Region_Count end = start + 5 for day in range(start,end): #print("Send weather day " + str(day)) #Print_Weather(day) memmove(addressof(Rs485_Buffer) + tx_len, byref(Daily_Weather[day]), 12) tx_len += 12 memmove(addressof(Rs485_Buffer) + tx_len, Daily_Weather[day].icon, 3) tx_len += 3 memmove(addressof(Rs485_Buffer) + tx_len, Daily_Weather[day].description, 33) tx_len += 33 Region_Count += 1 if Region_Count == len(Regions): Current_Action = NO_ACTION Region_Count = 0 elif Current_Action == SEND_HOURLY_DATA: DoLog("Send hourly data for region: " + Regions[Region_Count]) memmove(Rs485_Buffer, byref(HOURLY_WEATHER_DATA),1) memmove(addressof(Rs485_Buffer) + 1, byref(c_ubyte(Region_Count)),1) tx_len = 2 start = 5 * Region_Count end = start + 5 for slot in range(start,end): #Print_Hourly(slot) memmove(addressof(Rs485_Buffer) + tx_len, byref(Hourly_Weather[slot]), 12) tx_len += 12 memmove(addressof(Rs485_Buffer) + tx_len, Hourly_Weather[slot].icon, 3) tx_len += 3 memmove(addressof(Rs485_Buffer) + tx_len, Hourly_Weather[slot].description, 33) tx_len += 33 Region_Count += 1 if Region_Count == len(Regions): Current_Action = NO_ACTION Region_Count = 0 elif Current_Action == SEND_CURRENT_TIME: DoLog("Send UTC time") #current_time = str(datetime.now()) memmove(Rs485_Buffer, byref(CURRENT_TIME_DATA),1) memmove(addressof(Rs485_Buffer)+1, Utc_Time, 19) tx_len = 20 Current_Action = NO_ACTION elif Current_Action == SEND_WEEKDAY: DoLog("Send weekday") day_index = (c_byte)() day_index.value = date.weekday(date.today()) #print ("Today" + str(day_index.value)) memmove(Rs485_Buffer, byref(WEEKDAY_DATA),1) memmove(addressof(Rs485_Buffer)+1, byref(day_index), 1) tx_len = 2 Current_Action = NO_ACTION elif Current_Action == SEND_LOCATION: #print("Send location") memmove(Rs485_Buffer, byref(LOCATION_DATA),1) memmove(addressof(Rs485_Buffer)+1, Location, len(Location)) tx_len = 1 + len(Location) Current_Action = NO_ACTION elif Current_Action == SEND_LOCATION_CONFIRM: DoLog("Confirm location") memmove(Rs485_Buffer, byref(LOCATION_CONFIRM_DATA),1) tx_len = 1 Current_Action = NO_ACTION else: DoLog("Invalid Current Action") if tx_len: Rs485_Send(Rs485_Buffer, tx_len) Waiting_Ack_Failures = 0 Waiting_Ack = 1 sleep(0.01) return Is_Ack = 0 Rx_Index = 0 def Process_Rx(data, len): global Parse_State global Data_Len global Rx_Len global Waiting_Ack global Current_Action global Is_Ack global Rx_Index global Waiting_Ack_Failures #print("Rx: " + data.encode("hex")) #print("Rx len:" + str(len)) #print("State:" + str(Parse_State) + "Data_Len:" + str(Data_Len) + "Rx_Len:" + str(Rx_Len)) for index in range(len): if Parse_State == IDLE_STATE: if convert_int(data[index]) == RS485_START: #print("Found START") Parse_State = DST_ADDR_STATE Data_Len = 0 Rx_Len = 0 Is_Ack = 0 Rx_Index = 0 elif Parse_State == DST_ADDR_STATE: if convert_int(data[index]) == RASP_ADDR: #print("Correct Dst Addr") Parse_State = SRC_ADDR_STATE Rs485_Buffer[Rx_Index] = data[index] Rx_Index += 1 else: Parse_State = IDLE_STATE elif Parse_State == SRC_ADDR_STATE: #print("Src Addr: " + data[index].encode('hex')) Source_Addr = convert_int(data[index]) Rs485_Buffer[Rx_Index] = data[index] Rx_Index += 1 Parse_State = DATA_STATE elif Parse_State == DATA_STATE: if Data_Len == 0: Data_Len = convert_int(data[index]) #print("Pkg len: " + str(Data_Len)) Rx_Len = 0 Rs485_Buffer[Rx_Index] = data[index] Rx_Index += 1 else: Rs485_Buffer[Rx_Index] = data[index] Rx_Index += 1 #print("Waiting Ack:" + str(Waiting_Ack)) if Waiting_Ack == 1 or (Data_Len == 1 and convert_int(data[index]) == RS485_ACK): if convert_int(data[index]) == RS485_ACK: Is_Ack = 1 Parse_State = CRC_STATE else: #not ACK received as expected, so back to Idle state #print("Back Idle") Parse_State = IDLE_STATE Waiting_Ack_Failures += 1 if Waiting_Ack_Failures == MAX_ACK_FAILURES: #reset all since we fail to receive ACK Current_Action = NO_ACTION Waiting_Ack = 0 else: Rx_Len += 1 if Rx_Len == Data_Len: Parse_State = CRC_STATE elif Parse_State == CRC_STATE: #print("Check CRC: " + str(Rx_Index)) pkg_crc = convert_int(data[index]) crc = crc8(Rs485_Buffer, Rx_Index) if crc == pkg_crc: Parse_State = STOP_STATE else: Parse_State = IDLE_STATE DoLog("CRC failed") DoLog("PkgCrc: " + str(pkg_crc) + " CalculatedCrc: " + str(crc)) elif Parse_State == STOP_STATE: if convert_int(data[index]) == RS485_STOP: Parse_State = IDLE_STATE if Is_Ack: DoLog("Receive ACK") Waiting_Ack = 0 else: DoLog("Send ACK:" + str(Current_Action)) Sending_Ack() if Current_Action == NO_ACTION: Current_Action = Execute_Command(Rs485_Buffer, Data_Len+3) else: DoLog("Invalid Parsing state") return def Get_HourlyWeather(region, index): global Is_Internet_Available DoLog("Use owm.three_hours_forecast() to get hourly data for:" + Regions[index]) #Get current weather for Slot 0 fc = owm.weather_at_place(region) weather_slot0 = fc.get_weather() try: fc = owm.three_hours_forecast(region) except (owm.ParseResponseException, owm.APICallException): DoLog("Fail to fetch hourly data") Reset_HourlyWeatherData() Is_Internet_Available = 0 return DoLog("OWM done") if fc == None: #raise exception if no data returned raise Is_Internet_Available = 1 f = fc.get_forecast() slot_index = index * 5 end = slot_index + 5 reception_epoch = f.get_reception_time() is_first = 0 count=0 for weather in f: if count == 0: weather = weather_slot0 #hack to assign current weather to HourSlot0 count += 1 DoLog("Read hour slot:" + str(count)) temp = weather.get_temperature(unit='celsius') humid = weather.get_humidity() #humid = weather.get_clouds() wind = weather.get_wind()["speed"] rain = weather.get_rain() time = weather.get_reference_time(timeformat='iso') if is_first == 0: epoch = reception_epoch #to get current local time is_first = 1 else: epoch = weather.get_reference_time() #to get reference time Hourly_Weather[slot_index].hour = int(time[11]) * 10 + int(time[12]) Hourly_Weather[slot_index].temp = int(temp["temp"]) Hourly_Weather[slot_index].humid = int(humid) Hourly_Weather[slot_index].wind_int = int(wind) Hourly_Weather[slot_index].wind_frac = int((wind - int(wind))*100) Hourly_Weather[slot_index].epoch = epoch + Region_Utc_Zone[index]*3600 if "3h" in rain: temp = rain["3h"] Hourly_Weather[slot_index].rain_int = int(temp) Hourly_Weather[slot_index].rain_frac = int((temp - int(temp))*100) else: Hourly_Weather[slot_index].rain_int = 0 Hourly_Weather[slot_index].rain_frac = 0 Hourly_Weather[slot_index].description = weather.get_detailed_status() Hourly_Weather[slot_index].icon = (weather.get_weather_icon_name()).encode('ascii', 'ignore') slot_index += 1 if slot_index == end: break return def Get_DailyWeather(region, index): global Utc_Time global Epoch_Time global Is_Internet_Available DoLog("Use owm.daily_forecast() to get daily data for:" + Regions[index]) try: fc = owm.daily_forecast(region, limit=5) except (owm.ParseResponseException, owm.APICallException): DoLog("Fail to fetch daily data") Reset_DailyWeatherData() Is_Internet_Available = 0 return if fc == None: #raise exception if no data returned raise Is_Internet_Available = 1 DoLog("OWM done") f = fc.get_forecast() reception_epoch = f.get_reception_time() Utc_Time = f.get_reception_time('iso') #print Utc_Time day = 5*index; is_first = 0 count = 0 for weather in f: count += 1 DoLog("Read day:" + str(count)) temp = weather.get_temperature(unit='celsius') humid = weather.get_humidity() #humid = weather.get_clouds() wind = weather.get_wind()["speed"] rain = weather.get_rain() if is_first == 0: is_first = 1 epoch = reception_epoch #to get current local time else: epoch = weather.get_reference_time() Daily_Weather[day].temp = int(temp["day"]) Daily_Weather[day].min_temp = int(temp["min"]) Daily_Weather[day].max_temp = int(temp["max"]) Daily_Weather[day].wind_int = int(wind) Daily_Weather[day].wind_frac = int((wind - int(wind))*100) Daily_Weather[day].epoch = epoch + Region_Utc_Zone[index]*3600 if "all" in rain: temp = rain["all"] Daily_Weather[day].rain_int = int(temp) Daily_Weather[day].rain_frac = int((temp - int(temp))*100) else: Daily_Weather[day].rain_int = 0 Daily_Weather[day].rain_frac = 0 Daily_Weather[day].humid = humid Daily_Weather[day].description = weather.get_detailed_status() Daily_Weather[day].icon = (weather.get_weather_icon_name()).encode('ascii', 'ignore') #Print_Weather(day) day += 1 return def Fetch_Weather_Data(): global Weather_Data_Available #try: region_num = len(Regions) DoLog("Region num:" + str(region_num)) if region_num: for region_index in range(region_num): DoLog("Region:" + Regions[region_index]) Get_DailyWeather(Regions[region_index], region_index) Get_HourlyWeather(Regions[region_index], region_index) Weather_Data_Available = 1 DoLog("Done") ''' except: DoLog("Failed to execute Fetch_Weather_Data()") Reset_DailyWeatherData() Reset_HourlyWeatherData() Weather_Data_Available = 0 ''' return def Print_Hourly(slot): print("Epoch: " + str(Hourly_Weather[slot].epoch)) print("Hour:" + str(Hourly_Weather[slot].hour)) print("Temp:" + str(Hourly_Weather[slot].temp)) print("Humid:" + str(Hourly_Weather[slot].humid)) print("Rain int:" + str(Hourly_Weather[slot].rain_int)) print("Rain frac:" + str(Hourly_Weather[slot].rain_frac)) print("Wind int:" + str(Hourly_Weather[slot].wind_int)) print("Wind frac:" + str(Hourly_Weather[slot].wind_frac)) print("Icon:" + Hourly_Weather[slot].icon) print("Description:" + Hourly_Weather[slot].description) return def Print_Weather(day): print("Day:" + str(day)) print("Wind:") print (str(Daily_Weather[day].wind_int) + "." + str(Daily_Weather[day].wind_frac)) print("Rain:") print (str(Daily_Weather[day].rain_int) + "." + str(Daily_Weather[day].rain_frac)) print("Humidity:") print Daily_Weather[day].humid print("Temperature:") print Daily_Weather[day].temp print("Min Temp:") print Daily_Weather[day].min_temp print("Max temp:") print Daily_Weather[day].max_temp print("Icon:") print Daily_Weather[day].icon print("Description:") print Daily_Weather[day].description print("Epoch:") print Daily_Weather[day].epoch return def PrintCom(): for com in ComPort_List: DoLog(str(com)) return def Update_ComList(): ser = serial.Serial() ser.port = "/dev/ttyUSB0" try: ser.open() except serial.SerialException: ser = 0 if ser != 0: ser.close() #unload FTDI VCP driver os.system("sudo rmmod ftdi_sio") os.system("sudo rmmod usbserial") #Open again with D2XX driver and add to the list try: com = ftd2xx.open(0) except: return com.setBaudRate(115200) com.setLatencyTimer(2) ComPort_List.append(com) DoLog("############# FOUND ################") PrintCom() return def Reset_DailyWeatherData(): for index in range(20): Daily_Weather[index].temp = 0 Daily_Weather[index].min_temp = 0 Daily_Weather[index].max_temp = 0 Daily_Weather[index].wind_int = 0 Daily_Weather[index].wind_frac = 0 Daily_Weather[index].rain_int = 0 Daily_Weather[index].rain_frac = 0 Daily_Weather[index].humid = 0 Daily_Weather[index].description = "no data" Daily_Weather[index].icon = "10d" return def Reset_HourlyWeatherData(): for index in range(20): Hourly_Weather[index].hour = 0 Hourly_Weather[index].temp = 0 Hourly_Weather[index].humid = 0 Hourly_Weather[index].wind_int = 0 Hourly_Weather[index].wind_frac = 0 Hourly_Weather[index].rain_int = 0 Hourly_Weather[index].rain_frac = 0 Hourly_Weather[index].description = "no data" Hourly_Weather[index].icon = "10d" return if __name__ == "__main__": global Active_Com global Last_Fetch_Time global Is_Internet_Available try: os.system("sudo rmmod ftdi_sio") os.system("sudo rmmod usbserial") except: print("No VCP driver found") #Try to search any available D2XX devices for i in range(4): #Raspberry board has 4 USB ports try: com = ftd2xx.open(i) except ftd2xx.DeviceError: continue com.setBaudRate(115200) com.setLatencyTimer(2) ComPort_List.append(com) #add to the list PrintCom() DoLog("Start main loop") #enable if we want to check fetching OWM server #Region_Count = 4 #Regions = range(4) #Regions[0] = "Singapore" #Regions[1] = "Taipei" #Regions[2] = "Glasgow" #Regions[3] = "Ho Chi Minh" #while True: # Fetch_Weather_Data() # time.sleep(1) Last_Fetch_Time = time.time() while True: #Check for any hot-plug RS485 cable Update_ComList() #Check schedule to re-fetch weather data from OWM if Is_Internet_Available == 1: Refetch_Timeout = REFETCH_TIMEOUT_S else: Refetch_Timeout = 10 #retry every 10s to check internet status if time.time() - Last_Fetch_Time > Refetch_Timeout: DoLog("Refetching") Fetch_Weather_Data() Last_Fetch_Time = time.time() #Process data for all PanL devices one-by-one for com in ComPort_List: is_rx = 0 Active_COM = com #Check Rx from PanL rx_len = Active_COM.getQueueStatus() while rx_len: is_rx = 1 Rx_Data = Active_COM.read(rx_len) Process_Rx(Rx_Data, rx_len) Take_Action() sleep(0.05) rx_len = Active_COM.getQueueStatus() DoLog("Exit main loop\n") for com in ComPort_List: com.close() #Log_File.close() exit(1)