first
[purpleair-notify.git] / purpleair-notify.rb
blob9af128aff109cee49182e0f7af0ca77063730f6c
1 #!/usr/bin/env ruby
3 =begin
4 Copyright 2019 Wu Ming 2 at wu.ming2@icloud.com
6 This program is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program.  If not, see <https://www.gnu.org/licenses/>.
18 =end
20 require 'net/http'
21 require 'json'
22 require 'discordrb/webhooks'
23 require 'iron_cache'
25 # CONFIGURE THIS
26 WEBHOOK_URL = 'https://discordapp.com/api/webhooks/<your webhook>'
27 uri = URI('https://www.purpleair.com/json?show=<your ID>')
28 my_threshold = 15.0
29 who_day_threshold = 25.0        # WHO 2005 PM2.5 dayly average threshold w/ 3 days in a year allowance
30 who_year_threshold = 10.0       # WHO 2005 PM2.5 yearly average threshold
33 def fahrenheit_celsius(f)
34   celsius = (f - 32) * 5.0 / 9.0
35   celsius.round
36 end
38 def do_messaging(msg, flag = nil)
39         client = Discordrb::Webhooks::Client.new(url: WEBHOOK_URL)
40         begin
41                 client.execute do |builder|
42                         builder.content = msg
43                 end
44                 set_flag(flag) unless flag.nil? # for transaction
45         rescue RestClient::ExceptionWithResponse => e
46                 puts e.message
47                 # https://github.com/rest-client/rest-client
48                 # https://discordapp.com/developers/docs/topics/rate-limits#rate-limits
49         rescue Exception => e
50                 puts e.inspect
51         end
52 end
54 def get_cache
55         # NOTE: persistence expires after 7 days (up to 30) if untouched.  Then is deleted.
56         @ironcache = IronCache::Client.new
57         @cache = @ironcache.cache("purple_air") # created if not existant
58 end
60 def get_flag
61         item = @cache.get("is_up")
62         
63         # if expired    
64         if item.nil?
65                 @cache.put("is_up","below")
66                 value = "below"
67         else
68                 value = item.value 
69         end
70         value
71 end
73 def set_flag(flag)
74         @cache.put("is_up",flag)
75 end
77 def get_results(uri)
78         response = Net::HTTP.get(uri)
79         if response.empty?
80                 do_messaging("Service unavailable")
81                 exit(0)
82         end
83         JSON.parse(response)["results"]
84 end
86 def get_value(channel_a, channel_b, val)
87         value_a = JSON.parse(channel_a["Stats"])[val].to_f
88         value_b = JSON.parse(channel_b["Stats"])[val].to_f
89         value_a = value_a < 100 ? error(value_a , 10, kind = "abs") : error(value_a , 10)
90         value_b = value_b < 100 ? error(value_b , 10, kind = "abs") : error(value_b , 10)
91         value_min = ((value_a[0] + value_b[0])/2).to_i
92         value_max = ((value_a[1] + value_b[1])/2).to_i
93         return  value_min , value_max
94 end
96 def check_timing(stage, time)
97         case stage
98         when "starting"
99                 exit(0) if time.min.between?(01,19) || time.min.between?(31,49)
100         when "notify"
101                 exit(0) if time.hour.between?(00,05)
102         end
105 def to_average(n, avg, new_val) # unused for now
106         avg = avg + ( ( new_val - avg ) / n + 1 )
109 def error(value, err, kind = nil)
110         abs_error = kind.nil? ? (value/100)*err : err
111         return (value - abs_error).round, (value + abs_error).round
114 # check timing is correct
115 time = Time.now
116 check_timing("starting",time)
118 # get instance of persistance db
119 get_cache()
121 # values from https://docs.google.com/document/d/15ijz94dXJ-YAZLi9iZ_RaBwrZ4KtYeCy08goGBwnbCU/edit
122 # PM2.5 rolling averages:
123 #       v = current, v1 = 10' average, v2 = 30' average, 
124 #       v3 = 1hr average, v4 = 6hrs average, v5 = 1day average, v6 = 1wk average
125 # NOTE: to calculate 1 year avg - since intervals would be uneaven - 
126 #               interpolation and error estimate would be necessay
127 # PMS 5003 laser counters, 
128 # http://www.aqmd.gov/docs/default-source/aq-spec/resources-page/plantower-pms5003-manual_v2-3.pdf
129 # Laser Sensor capabilities:
130 #       Effective Range: 0~500 μg/m³ 
131 #       Counting Efficiency: 50%@0.3μm 98%@>=0.5μm
132 #               => useless below 0.5 μm
133 #       Resolution: 1 μg/m³
134 #               => to_i necessary  
135 #       Maximum Consistency Error: ±10μg/m³@0~100μg/m³, ±10%@100~500μg/m³
136 results = get_results(uri)
137 channel_a = results[0] # necessary later
138 channel_b = results[1]
139 current_value = get_value(channel_a, channel_b, "v1")
140 day_avg = get_value(channel_a, channel_b, "v5")
141 week_avg = get_value(channel_a, channel_b, "v6")
143 # check timing is correct after updating averages (TBD)
144 check_timing("notify",time)
146 age = channel_a["AGE"]
147 failure = channel_a["A_H"]
148 temperature = channel_a["temp_f"].to_f - 8 # to compensate for casing effect
149 temperature = fahrenheit_celsius(temperature)
150 humidity = channel_a["humidity"].to_f * 1.04 # to compensate for casing effect
151 humidity = error(humidity, 3) # Accuracy tolerance: ± 3 % relative humidity
152 humidity_msg = "needs active regulation!!" if humidity[1] > 70 || humidity[0] < 30
154 # possible old data
155 do_messaging("Data #{age} minutes old") if age > 10
157 # possible hw failure msg
158 do_messaging("Suspected hardware issues with message: #{failure}") unless failure.nil?
160 # get the up/down flag
161 flag = get_flag()
163 # if notified already
164 exit(0) if current_value[1] <= my_threshold && flag == "below"
165 #exit(0) if current_value[0] > my_threshold && flag == "above"  want continuous notifications
167 if current_value[1] <= my_threshold && flag == "above"
168         # notify to stop
169         msg = "Averages: \n ten min #{current_value.to_s}  day #{day_avg.to_s}  week #{week_avg.to_s} "
170         msg << "\n Temperature #{temperature}°C Humidity #{humidity.to_s} #{humidity_msg}"
171         do_messaging(msg, "below")
174 if current_value[0] > my_threshold # && flag == "below"
175         # notify to start
176         msg = "START. "
177         msg << "Averages: \n ten min #{current_value.to_s}  day #{day_avg.to_s}  week #{week_avg.to_s} "
178         msg << "\n Temperature #{temperature}°C Humidity #{humidity.to_s}% #{humidity_msg}"
179         do_messaging(msg, "above")
183 exit(0)