1 #!/usr/bin/env ruby --jit
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/>.
31 MY_THRESHOLD = 10.0 # PM2.5 µg/m³
34 DISCORD_CHANNEL_ID = <Channel ID>
35 DISCORD_AUTHOR_ID = <Your ID> # just one author for now
36 DISCORD_TOKEN = '<Token>'
37 DISCORD_HISTORY_SIZE = 10 # increase if more chatty
39 PA_URI = URI('https://www.purpleair.com/json?show=<Your PA ID>')
41 # Implement Observatory module after updating this
42 OB_URI = URI('<Alternate source of T and Humidity source URL>')
43 LOCATION = "<Alternate source of T and Humidity source display name>"
48 TIME_10_FLAG = TIME.min.between?(01,19)
49 TIME_40_FLAG = TIME.min.between?(31,49)
50 TIME_BEGIN_PREV = TIME-30*60
52 NIGHTTIME_FLAG = TIME.hour.between?(00,05)
53 # night period boundaries
54 EXP_REF = NIGHTTIME_FLAG ? 5 : 0
55 DAILY_FLAG = TIME.hour == 8
56 WEEKLY_FLAG = DAILY_FLAG && TIME.sunday?
59 MESSAGING = {"!start" => false, "!stop" => true}
61 # General exlusion period
62 exit(0) if Config::TIME_10_FLAG || Config::TIME_40_FLAG
66 # Keeps quiet if user wishes so or at night time
67 MessagesNotAllowedNow = proc do |cached_wish|
68 cached_wish ||= CacheControl::GetWish.call
70 # user wish always prevails if not nil
71 # dynamic flag before | after Discord update
72 [ Config::MESSAGING[cached_wish], Config::NIGHTTIME_FLAG ].compact.first
74 IsAboveThreshold = proc {Purple::Current > Config::MY_THRESHOLD}
75 WasBelowThreshold = proc {Purple::Hour_Avg <= Config::MY_THRESHOLD}
77 # Avoids messaging if notified already below threshold
78 proc {!IsAboveThreshold.call && WasBelowThreshold.call}
79 IsHumidityOutOfRange = proc {!Observatory::Humidity.to_i.between?(30,70)}
83 result = JSON.parse(File.read('purple_dump.json'))["results"][0] # Hash
84 res_a_keys = result.keys # Array
85 stats_a_keys = JSON.parse(result["Stats"]).keys # Array
86 check_diff = proc do |keys1, keys2|
87 Hashdiff.diff(keys1,keys2)
90 # Just to try currying
91 Check_res = check_diff.curry.call(res_a_keys)
92 Check_stats = check_diff.curry.call(stats_a_keys)
96 #Bot = Discordrb::Bot.new(token: Config::DISCORD_TOKEN, ignore_bots: true)
97 #Channel = Bot.channel(Config::DISCORD_CHANNEL_ID)
98 PurpleData = Net::HTTP.get(Config::PA_URI)
103 # Discordrb::Bot is not intended to be used without bot.run
104 #Start = proc {Conn::Bot.run(:async)} # returns Proc instead of executing at assignment
105 #Stop = proc {Conn::Bot.stop; exit(0)} # returns an ERROR message
106 #Send = proc {|msg| Conn::Bot.send_message(Conn::Channel, msg)}
107 #Quit = proc {|msg| (Stop<<Send).call(msg)}
108 response = Discordrb::API::Channel.messages( # gets last messages
109 "Bot "+Config::DISCORD_TOKEN, Config::DISCORD_CHANNEL_ID,
110 Config::DISCORD_HISTORY_SIZE ) # newest first
111 parse = proc { |resp| JSON.parse(resp) } # RestClient::Response -> [(0..n)Hash]
112 by_time = proc { |a| # [(0..n)Hash] -> [(0..n)Hash]
114 Time.parse(m["timestamp"]).getlocal >= Config::TIME_BEGIN_PREV } }
115 by_author = proc { |a| # [(0..n)Hash] -> [(0..n)Hash]
117 m["author"]["id"] == Config::DISCORD_AUTHOR_ID.to_s } }
118 by_content = proc { |a| # [(0..n)Hash] -> nil or Str
119 Config::MESSAGING.keys.find { |k|
120 a.any? { |m| m["content"].start_with?(k) } } }
123 Send = proc {|msg| Discordrb::API::Channel.create_message(
124 "Bot "+Config::DISCORD_TOKEN, Config::DISCORD_CHANNEL_ID, msg )}
125 Wish = (by_content<<by_author<<by_time<<parse).call(response)
129 getFlag = proc {|flag| Conn::Red.get(flag)} # -> nil or Str
133 # from midnight of tomorrow or
135 d = Config::EXP_REF == 0 ? Date.today + 1 : Date.today
136 # return expiration time in sec from REF
137 ( Time.new(d.year,d.month,d.day,Config::EXP_REF,0,0) - t ).to_i
139 # User wish for start/stop
140 GetWish = proc {getFlag.call("messaging")} # changes w update
141 setWish = proc do |wish,cached_wish|
143 # valid user wish is always opposite of time-based flag
144 # if cache doesn't exist and is valid create cache
145 # if cache exist is opposite already so delete
147 Conn::Red.set("messaging", wish, :ex => expSec) :
148 Conn::Red.del("messaging")
150 userWish = proc do |user_wish|
151 # see .gv file for logic
152 # More clever solution for binary decision tree surely exist.
153 next unless user_wish # no news from user, returns nil
154 cached_wish = GetWish.call
155 # cached flag or time-based flag
156 sys_wish = Proceed::MessagesNotAllowedNow.call(cached_wish)
157 next if sys_wish == Config::MESSAGING[user_wish] # if old or silly news
158 BotControl::Send.call("#{user_wish} got it! Until next period change.")
159 [user_wish,cached_wish]
160 # returns nil or [user,nil] or [user,cached]
162 autoUpdate = (setWish<<userWish).call(BotControl::Wish)
166 results = JSON.parse(Conn::PurpleData)["results"] # [(2)]
167 stats_a = JSON.parse(results[0]["Stats"]) # Hash
168 stats_b = JSON.parse(results[1]["Stats"]) # Hash
169 value = proc {|key| (stats_a[key].to_f + stats_b[key].to_f) / 2}
171 # check if data format changed
172 Res_diff = NeedToKnow::Check_res.call(results[0].keys)
173 Stats_diff = NeedToKnow::Check_stats.call(stats_a.keys)
176 Age = results[0]["AGE"]
177 Current = value.call("v1")
178 Hour_Avg = value.call("v3")
179 Day_Avg = value.call("v5")
180 Week_Avg = value.call("v6")
181 #Failure = A_G not available anymore??
185 # low resilience to page formatting changes
186 Temperature, Humidity = Net::HTTP.get(Config::OB_URI).lines.<implement>
190 msg = Proceed::IsAboveThreshold.call ? "START" : "OK"
191 humidity = Proceed::IsHumidityOutOfRange.call ? "Humidity needs active regulation!!" : ""
192 particulate = "Average: last ten min #{Purple::Current.to_i.to_s}."
193 temperature = "Temperature #{Observatory::Temperature.to_s}°C in #{Config::LOCATION}."
194 daily = Config::DAILY_FLAG ? "Average: last day #{Purple::Day_Avg.to_i.to_s}." : ""
195 weekly = Config::WEEKLY_FLAG ? "Average: last week #{Purple::Week_Avg.to_i.to_s}." : ""
196 res_diff = Purple::Res_diff.empty? ? "" : "Channel changed: #{Purple::Res_diff}"
197 stats_diff = Purple::Stats_diff.empty? ? "" : "Stats changed: #{Purple::Stats_diff}"
201 msg + " " + particulate + " " + humidity + " " +
202 temperature + " " + daily + " " + weekly + " " +
203 res_diff + " " + stats_diff
205 Age = Purple::Age > 10 ? "Data #{Purple::Age} minutes old." : ""
206 #Failure = "Possible hardware issue: #{@readings[:failure]}" if Purple::Failure??