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/>.
22 require 'discordrb/webhooks'
26 WEBHOOK_URL = 'https://discordapp.com/api/webhooks/<your webhook>'
27 uri = URI('https://www.purpleair.com/json?show=<your ID>')
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
38 def do_messaging(msg, flag = nil)
39 client = Discordrb::Webhooks::Client.new(url: WEBHOOK_URL)
41 client.execute do |builder|
44 set_flag(flag) unless flag.nil? # for transaction
45 rescue RestClient::ExceptionWithResponse => e
47 # https://github.com/rest-client/rest-client
48 # https://discordapp.com/developers/docs/topics/rate-limits#rate-limits
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
61 item = @cache.get("is_up")
65 @cache.put("is_up","below")
74 @cache.put("is_up",flag)
78 response = Net::HTTP.get(uri)
80 do_messaging("Service unavailable")
83 JSON.parse(response)["results"]
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
96 def check_timing(stage, time)
99 exit(0) if time.min.between?(01,19) || time.min.between?(31,49)
101 exit(0) if time.hour.between?(00,05)
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
116 check_timing("starting",time)
118 # get instance of persistance db
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³
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
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
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"
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"
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")