Promixis
April 26th, 2005, 02:29 PM
Ok, I have finally got something worth using. This will only work on the A13 release. Please do not modify the code below. Only access the data using the provided methods. The core code may change but the structure of the returned data should not. This code will eventually be moved into G4. Use the variable inspector to see the data that is returned.
--[[
Weather Object
Object/class for retreiving the weather from customweather.com
(c) copyright Promixis LLC
NOTES: This object uses threads. You must only access the data in the object using the object mutex.
3 Get functions (which take care of the mutex) are provided to get current conditions,
forecast conditions and satelite map filename.
--]]
CWWeatherRetrieval = {
-- URL Request settings (constant) ....
BaseURL = "http://xml.customweather.com/xml",
ClientURL = "client=proximis_test&client_password=t3mp",
LocationURL = nil, -- declared in each object (ie not constant)
CCProductURL = "product=current_conditions",
FCProductURL = "product=expanded_forecast",
SatMapProductURL = "product=maps&map_product=satellite",
UnitURL = "metric=",
New = function (self,o)
o = o or {}
setmetatable (o,self)
self.__index = self
return o
end,
Start = function (self)
-- setup locationurl to retreive weather
if not self.LocationURL then -- allows external control over location lookup ie, won't overwrite a preset url
if self.CWID then
self.LocationURL = "id="..self.CWID
elseif self.Location.Zipcode and self.Location.Country == "United States" then
self.LocationURL = "zip_code="..self.Location.Zipcode
elseif self.Location.City then
self.LocationURL = "name="..self.Location.City
if self.Location.State then
self.LocationURL = self.LocationURL ..","..self.Locaton.State
end
if self.Location.Country then
self.LocationURL = self.LocationURL ..","..self.Locaton.Country
end
elseif self.Location.Latitude and self.Location.Longitude then
self.LocationURL = "latitude="..self.Location.Latitude.."&longitude="..self.Location.Longitude
else
return nil -- no location given to do the look up....
end
end
self.NotificationCallbacks = {} -- table of callback functions
self.Mutex = thread.newmutex () -- for async retreivals...
-- misc settings
if self.Location.Country == "United States" then
self.Metric = self.Metric or false
else -- non us
self.Metric = self.Metric or true
end
self.AutoUpdate = self.AutoUpdate or false -- set to true to enable autoupdates
self.SatMapAnimation = self.SatMapAnimation or false
-- weather data tables
self.CC = {
RefreshInterval = 15, -- minutes
TimeStamp = {},
LastUpdate = 0,
URL = "",
Product = "Current Conditions",
Data = nil,
}
self.FC = {
RefreshInterval = 4*60, --minutes
TimeStamp = {},
LastUpdate = 0,
URL = 0,
Product = "Forecast Conditions",
Data = nil,
}
self.SatMap = {
RefreshInterval = 30, --minutes
LastUpdate = 0,
URL = 0,
Product = "Satellite Map",
Filename = win.GetTempFilename (win.GetTempPath (),"CSM",0),
}
if not self.SatMap.Filename then
return
end
-- setup urls
self.CC.URL = self.BaseURL.."?"..self.ClientURL.."&"..self.CCProductURL.."&"..self.LocationURL.."&"..self.UnitURL..tostring (self.Metric)
self.FC.URL = self.BaseURL.."?"..self.ClientURL.."&"..self.FCProductURL.."&"..self.LocationURL.."&"..self.UnitURL..tostring (self.Metric)
self.SatMap.URL = self.BaseURL.."?"..self.ClientURL.."&"..self.SatMapProductURL.."&"..self.LocationURL.."&anim="..tostring(self.SatMapAnimation)
if self.AutoUpdate then
self.AutoUpdateTimer = gir.CreateTimer (function (...) return self:UpdateCheck (unpack (arg)) end,function (...) return self:UpdateCheck (unpack (arg)) end,nil,1)
self.AutoUpdateTimer:Arm (60000)--check every minute
end
return true
end,
UpdateCC = function (self)
local error,status,data
gir.CoInitialize ()
local WeatherDOM = luacom.CreateObject("Msxml2.DOMDocument")
if WeatherDOM then
WeatherDOM.async = false
-- note luacom error settings may change the way this function behaves...
status,error = pcall ( function (...) return WeatherDOM:load(self.CC.URL) end)
if status then -- no error
if WeatherDOM.parseError.errorCode == 0 then -- no error
data,error = self:ParseCC (WeatherDOM)
else
error = WeatherDOM.parseError.reason
end
end
else
error = "DOM Failure"
end
if data then -- we got data
local timestamp
if data.Report then
local h = string.find ((data.Report.localupdatetime or""),"..:..:..")
if h then
local months = {Jan=1,Feb=2,Mar=3,Apr=4,May=5,Jun=6,Jul=7,Aug=8,S ep=9,Oct=10,Nov=11,Dec=12}
timestamp = {Hour = tonumber (string.sub (data.Report.localupdatetime,h,h+1)),
Minute = tonumber (string.sub (data.Report.localupdatetime,h+3,h+4)),
Second = tonumber (string.sub (data.Report.localupdatetime,h+6,h+7)),
Year = tonumber (string.sub (data.Report.localupdatetime,13,16)),
Day = tonumber (string.sub (data.Report.localupdatetime,6,7)),
Month = tonumber (months [string.sub (data.Report.localupdatetime,9,11)]) }
end
end
if not self.CC.Data or not self.CC.Data.Report or self.CC.Data.Report.localupdatetime ~= data.Report.localupdatetime then -- we have an update
self.CC.Data = data
self.CC.TimeStamp = timestamp
self:LogUpdate (self.CC.Product)
self.CC.LastUpdate = win.GetElapsedSeconds ()
else
--print ("update not new")
end
else
self:LogUpdate (self.CC.Product,(error or "unknown error"))
end
gir.CoUninitialize ()
WeatherDOM = nil
collectgarbage ()
end,
ParseCC = function (self,WeatherDOM)
local data = {}
local rn = WeatherDOM:selectSingleNode ("report")
if not rn then
rn = WeatherDOM:selectSingleNode ("cw_report")
if rn then
return nil,rn.text
end
return nil,"parsing error"
end
data.Report = self:ParseAttributes (rn.attributes)
if rn and rn:hasChildNodes () then -- observations
data.Observations = {}
local cns = rn.childNodes
for j = 0, cns.length-1 do
data.Observations [j+1] = self:ParseAttributes (cns:item (j).attributes)
end
end
return data
end,
UpdateCCAsync = function (self)
self.Mutex:lock ()
gir.CoInitialize ()
self.CCThreadID = thread.newthread (self.UpdateCC,{self})
gir.CoUninitialize ()
self.Mutex:unlock ()
end,
PrintCC = function (self) -- NOT THREAD SAFE
if self.CC.Data and self.CC.Data.Report then
print ("Current Conditions")
print ("| Location")
for x,y in pairs (self.CC.Data.Report) do
print ("| ",x,": ",y)
end
end
if self.CC.Data and self.CC.Data.Observations then
for i = 1, table.getn (self.CC.Data.Observations) do
print ("| Observation #:",i)
for x,y in pairs (self.CC.Data.Observations [i]) do
print ("| ",x,": ",y)
end
end
end
end,
GetCC = function (self)
self.Mutex:lock ()
local data = {}
for a,b in pairs (self.CC.Data) do
data [a] = b
end
self.Mutex:unlock ()
return data
end,
UpdateFC = function (self)
local error,status,data
gir.CoInitialize ()
local WeatherDOM = luacom.CreateObject("Msxml2.DOMDocument")
if WeatherDOM then
WeatherDOM.async = false
-- note luacom error settings may change the way this function behaves...
status,error = pcall ( function (...) return WeatherDOM:load(self.FC.URL) end)
if status then -- no error
if WeatherDOM.parseError.errorCode == 0 then -- no error
data,error = self:ParseFC (WeatherDOM)
else
error = WeatherDOM.parseError.reason
end
end
else
error = "DOM Failure"
end
if data then -- we got data
local timestamp
if data.Report then
local h = string.find ((data.Report.localupdatetime or""),"..:..:..")
if h then
local months = {Jan=1,Feb=2,Mar=3,Apr=4,May=5,Jun=6,Jul=7,Aug=8,S ep=9,Oct=10,Nov=11,Dec=12}
timestamp = {Hour = tonumber (string.sub (data.Report.localupdatetime,h,h+1)),
Minute = tonumber (string.sub (data.Report.localupdatetime,h+3,h+4)),
Second = tonumber (string.sub (data.Report.localupdatetime,h+6,h+7)),
Year = tonumber (string.sub (data.Report.localupdatetime,13,16)),
Day = tonumber (string.sub (data.Report.localupdatetime,6,7)),
Month = tonumber (months [string.sub (data.Report.localupdatetime,9,11)]) }
end
end
if not self.FC.Data or not self.FC.Data.Report or self.FC.Data.Report.localupdatetime ~= data.Report.localupdatetime then -- we have an update
self.FC.Data = data
self.FC.TimeStamp = timestamp
self:LogUpdate (self.FC.Product)
self.FC.LastUpdate = win.GetElapsedSeconds ()
else
-- print ("update not new")
end
else
self:LogUpdate (self.FC.Product,(error or "unknown error"))
end
gir.CoUninitialize ()
WeatherDOM = nil
collectgarbage ()
end,
ParseFC = function (self,WeatherDOM)
local data = {}
local rn = WeatherDOM:selectSingleNode ("report")
if not rn then
return
end
data.Report = self:ParseAttributes (rn.attributes)
local ln = rn:selectSingleNode ("location")
if not ln then
return
end
if ln and ln:hasChildNodes () then -- observations
data.Location = self:ParseAttributes (ln.attributes)
data.Forecast = {}
local lns = ln.childNodes
for j = 0, lns.length-1 do
data.Forecast [j+1] = self:ParseAttributes (lns:item (j).attributes)
end
end
return data
end,
UpdateFCAsync = function (self)
self.Mutex:lock ()
gir.CoInitialize ()
self.FCThreadID = thread.newthread (self.UpdateFC,{self})
gir.CoUninitialize ()
self.Mutex:unlock ()
end,
PrintFC = function (self) -- NOT THREAD SAFE
if self.FC and self.FC.Report then
print ("Forecast Conditions")
print ("| Report")
for x,y in pairs (self.FC.Report) do
print ("| ",x,": ",y)
end
end
if self.FC and self.FC.Location then
print ("| Location")
for x,y in pairs (self.FC.Location) do
print ("| ",x,": ",y)
end
for i = 1, table.getn (self.FC.Forecast) do
print ("| Forecast Day #:",i)
for x,y in pairs (self.FC.Forecast [i]) do
print ("| ",x,": ",y)
end
end
end
end,
GetFC = function (self)
self.Mutex:lock ()
local data = {}
for a,b in pairs (self.FC.Data)do
data [a] = b
end
self.Mutex:unlock ()
return data
end,
ParseAttributes = function (self,attributes)
local t = {}
for x = 0, attributes.length - 1 do
t [attributes:item (x).name] = attributes:item (x).value
end
return t
end,
UpdateSatMap = function (self)
local http=require("socket.http")
local b,HTTPERROR=http.request( self.SatMap.URL )
if HTTPERROR == 200 then -- no error
local f = assert(io.open(self.SatMap.Filename, "wb"))
local t = f:write(b)
f:close()
self.SatMap.LastUpdate = win.GetElapsedSeconds ()
self:LogUpdate (self.SatMap.Product)
else
self:LogUpdate (self.SatMap.Product,"Error "..(HTTPERROR or "unknown"))
end
end,
GetSatMapFilename = function (self)
self.Mutex:lock ()
local fn = self.SatMap.Filename
self.Mutex:unlock ()
return fn
end,
UpdateSatMapAsync = function (self)
self.Mutex:lock ()
self.SatMapThreadID = thread.newthread (self.UpdateSatMap,{self})
self.Mutex:unlock ()
end,
UpdateCheck = function (self)
if self.UpdateThreadID and self.UpdateThreadID:isthreadrunning () then
return
end
self.UpdateThreadID = thread.newthread (self.UpdateCheckAsync,{self})
end,
UpdateCheckAsync = function (self)
self.Mutex:lock () -- note, an error between the locked mutex means a girder reboot...
--we check time since last update and the expected time of the update as per CW guidelines
if self.CC.LastUpdate == 0 or win.GetElapsedSeconds (self.CC.LastUpdate) / 60 > self.CC.RefreshInterval then
-- do not use below, this results in too aggressive timing the timestamp is too far out of date with the actual post time....
--(win.DateToFileTime (self.CC.TimeStamp) or 0) + self.CC.RefreshInterval * 60 < win.DateToFileTime (win.GetLocalTime ()) then
if not pcall (function (...) return self:UpdateCC (unpack (arg)) end) then -- something bad happened
print ("Error in Current Condition Update")
end
end
if self.FC.LastUpdate == 0 or win.GetElapsedSeconds (self.FC.LastUpdate) / 60 > self.FC.RefreshInterval or
-- note the 1.2 below, this is a fudge factor to limit too many request before new data is ready...
(win.DateToFileTime (self.FC.TimeStamp) or 0)+ self.FC.RefreshInterval * 60 *1.2 < win.DateToFileTime (win.GetLocalTime ()) then
if not pcall (function (...) return self:UpdateFC (unpack (arg)) end) then -- something bad happened
print ("Error in Forecast Condition Update")
end
end
--we don't know the time the map should be updating as we don't get this info from CW when the next update is due....
if self.SatMap.LastUpdate == 0 or win.GetElapsedSeconds (self.SatMap.LastUpdate) /60 > self.SatMap.RefreshInterval then
if not pcall (function (...) return self:UpdateSatMap (unpack (arg)) end) then -- something bad happened
print ("Error in Satelite Map Update")
end
end
self.Mutex:unlock ()
end,
LogUpdate = function (self,ProductID,Error) -- default callback
if not Error then
gir.LogMessage ("Weather",ProductID.." updated for "..self.Location.City,3)
else
gir.LogMessage ("Weather",ProductID.." error for "..self.Location.City.. (Error or ""),1)
end
for a,b in self.NotificationCallbacks do
gir.RunCodeOnMainThread (b,ProductID,Error) -- note, runcodeonmainthread is an sync call which is good for COM objects getting data back. This is an ASYNC cal
end
end,
-- functions to call whenever weather is updated. calls function with productid plus error if one occurred.
AddNotificationCallback = function (self,f)
self.Mutex:lock ()
self.NotificationCallbacks [f] = f
self.Mutex:unlock ()
end,
RemoveNotificationCallback = function (self,f)
self.Mutex:lock ()
self.NotificationCallbacks [f] = nil
self.Mutex:unlock ()
end,
Quit = function (self) -- must call this to dispose of properly or do a script reset.
local timeout = win.GetElapsedSeconds ()
while self.UpdateThreadID and self.UpdateThreadID:isthreadrunning () and win.GetElapsedSeconds (timeout) < 60 do
win.Sleep (50)
end
self.Mutex:lock ()
self.AutoUpdateTimer:Destroy ()
self.AutoUpdateTimer = nil
self.Mutex:unlock ()
-- self = nil
collectgarbage ()
end
}
if LocalWeather then
LocalWeather:Quit ()
end
LocalWeather = CWWeatherRetrieval:New ({Location = gir.GetLocation (),AutoUpdate = true,SatMapAnimation = true})
if not LocalWeather:Start () then
gir.LogMessage ("Weather","Unable to start",1)
end
Powered by vBulletin® Version 4.2.0 Copyright © 2013 vBulletin Solutions, Inc. All rights reserved.