/*
Weather Protocol plugin for Miranda IM
Copyright (c) 2012 Miranda NG team
Copyright (c) 2005-2011 Boris Krasnovskiy All Rights Reserved
Copyright (c) 2002-2005 Calvin Che
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; version 2
of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
/*
This file contain the source related to updating new weather
information, both automatic (by timer) and manually (by selecting
menu items).
*/
#include "stdafx.h"
/////////////////////////////////////////////////////////////////////////////////////////
// retrieve weather info and display / log them
// hContact = current contact
int CWeatherProto::UpdateWeather(MCONTACT hContact)
{
DBVARIANT dbv;
BOOL Ch = FALSE;
if (hContact == NULL) // some error prevention
return 1;
dbv.pszVal = "";
// log to netlib log for debug purpose
Netlib_LogfW(m_hNetlibUser, L"************************************************************************");
int dbres = getWString(hContact, "Nick", &dbv);
Netlib_LogfW(m_hNetlibUser, L"<-- Start update for station -->");
// download the info and parse it
// result are stored in database
int code = GetWeatherData(hContact);
if (code != 0) {
// error occurs if the return value is not equals to 0
if (opt.ShowWarnings) {
// show warnings by popup
CMStringW str(FORMAT, TranslateT("Unable to retrieve weather information for %s"), dbv.pwszVal);
str.AppendChar('\n');
str.Append(ptrW(GetError(code)));
WPShowMessage(str, SM_WARNING);
}
// log to netlib
Netlib_LogfW(m_hNetlibUser, L"Error! Update cannot continue... Start to free memory");
Netlib_LogfW(m_hNetlibUser, L"<-- Error occurs while updating station: %s -->", dbv.pwszVal);
if (!dbres)
db_free(&dbv);
return 1;
}
if (!dbres)
db_free(&dbv);
// initialize, load new weather Data
WEATHERINFO winfo = LoadWeatherInfo(hContact);
// translate weather condition
mir_wstrcpy(winfo.cond, TranslateW(winfo.cond));
// compare the old condition and determine if the weather had changed
if (opt.UpdateOnlyConditionChanged) { // consider condition change
if (!getWString(hContact, "LastCondition", &dbv)) {
if (mir_wstrcmpi(winfo.cond, dbv.pwszVal)) Ch = TRUE; // the weather condition is changed
db_free(&dbv);
}
else Ch = TRUE;
if (!getWString(hContact, "LastTemperature", &dbv)) {
if (mir_wstrcmpi(winfo.temp, dbv.pwszVal)) Ch = TRUE; // the temperature is changed
db_free(&dbv);
}
else Ch = TRUE;
}
else { // consider update time change
if (!getWString(hContact, "LastUpdate", &dbv)) {
if (mir_wstrcmpi(winfo.update, dbv.pwszVal)) Ch = TRUE; // the update time is changed
db_free(&dbv);
}
else Ch = TRUE;
}
// have weather alert issued?
dbres = db_get_ws(hContact, WEATHERCONDITION, "Alert", &dbv);
if (!dbres && dbv.pwszVal[0] != 0) {
if (opt.AlertPopup && !getByte(hContact, "DPopUp") && Ch) {
// display alert popup
CMStringW str(FORMAT, L"Alert for %s%c%s", winfo.city, 255, dbv.pwszVal);
WPShowMessage(str, SM_WEATHERALERT);
}
// alert issued, set display to italic
if (opt.MakeItalic)
setWord(hContact, "ApparentMode", ID_STATUS_OFFLINE);
Skin_PlaySound("weatheralert");
}
// alert dropped, set the display back to normal
else delSetting(hContact, "ApparentMode");
if (!dbres) db_free(&dbv);
// backup current condition for checking if the weather is changed or not
setWString(hContact, "LastLog", winfo.update);
setWString(hContact, "LastCondition", winfo.cond);
setWString(hContact, "LastTemperature", winfo.temp);
setWString(hContact, "LastUpdate", winfo.update);
// display condition on contact list
int iStatus = MapCondToStatus(winfo.hContact);
if (opt.DisCondIcon && iStatus != ID_STATUS_OFFLINE)
setWord(hContact, "Status", ID_STATUS_ONLINE);
else
setWord(hContact, "Status", iStatus);
AvatarDownloaded(hContact);
db_set_ws(hContact, "CList", "MyHandle", GetDisplay(&winfo, GetTextValue('C')));
CMStringW str2(GetDisplay(&winfo, GetTextValue('S')));
if (!str2.IsEmpty())
db_set_ws(hContact, "CList", "StatusMsg", str2);
else
db_unset(hContact, "CList", "StatusMsg");
ProtoBroadcastAck(hContact, ACKTYPE_AWAYMSG, ACKRESULT_SUCCESS, nullptr, (LPARAM)(str2.IsEmpty() ? nullptr : str2.c_str()));
// save descriptions in MyNotes
db_set_ws(hContact, "UserInfo", "MyNotes", GetDisplay(&winfo, GetTextValue('N')));
db_set_ws(hContact, WEATHERCONDITION, "WeatherInfo", GetDisplay(&winfo, GetTextValue('X')));
// set the update tag
setByte(hContact, "IsUpdated", TRUE);
// logging
if (Ch) {
// play the sound event
Skin_PlaySound("weatherupdated");
if (getByte(hContact, "File")) {
// external log
if (!getWString(hContact, "Log", &dbv)) {
// for the option for overwriting the file, delete old file first
if (getByte(hContact, "Overwrite"))
DeleteFile(dbv.pwszVal);
// open the file and set point to the end of file
FILE *file = _wfopen(dbv.pwszVal, L"a");
db_free(&dbv);
if (file != nullptr) {
// write data to the file and close
fputws(GetDisplay(&winfo, GetTextValue('E')), file);
fclose(file);
}
}
}
if (getByte(hContact, "History")) {
// internal log using history
T2Utf szMessage(GetDisplay(&winfo, GetTextValue('H')));
DBEVENTINFO dbei = {};
dbei.szModule = m_szModuleName;
dbei.iTimestamp = (uint32_t)time(0);
dbei.flags = DBEF_READ | DBEF_UTF;
dbei.eventType = EVENTTYPE_MESSAGE;
dbei.pBlob = szMessage;
dbei.cbBlob = (uint32_t)mir_strlen(szMessage) + 1;
db_event_add(hContact, &dbei);
}
// show the popup
WeatherPopup(hContact, Ch);
}
Netlib_LogfW(m_hNetlibUser, L"Update Completed - Start to free memory");
Netlib_LogfW(m_hNetlibUser, L"<-- Update successful for station -->");
// Update frame data
UpdateMwinData(hContact);
// update brief info if its opened
HWND hMoreDataDlg = WindowList_Find(hDataWindowList, hContact);
if (hMoreDataDlg != nullptr)
PostMessage(hMoreDataDlg, WM_UPDATEDATA, 0, 0);
return 0;
}
/////////////////////////////////////////////////////////////////////////////////////////
// a linked list queue for updating weather station
// this function add a weather contact to the end of queue for update
// hContact = current contact
void CWeatherProto::UpdateListAdd(MCONTACT hContact)
{
mir_cslock lck(m_csUpdate);
m_updateList.push_back(hContact);
}
// get the first item from the update queue and remove it from the queue
// return value = the contact for next update
MCONTACT CWeatherProto::UpdateGetFirst()
{
mir_cslock lck(m_csUpdate);
if (m_updateList.empty())
return 0;
auto it = m_updateList.begin();
MCONTACT hContact = *it;
m_updateList.erase(it);
return hContact;
}
void CWeatherProto::DestroyUpdateList(void)
{
mir_cslock lck(m_csUpdate);
m_updateList.clear();
}
/////////////////////////////////////////////////////////////////////////////////////////
// update all weather thread
// this thread update each weather station from the queue
void CWeatherProto::UpdateThread(void *)
{
{ mir_cslock lck(m_csUpdate);
if (m_bThreadRunning)
return;
m_bThreadRunning = true; // prevent 2 instance of this thread running
}
// update weather by getting the first station from the queue until the queue is empty
while (!m_updateList.empty() && !Miranda_IsTerminated())
UpdateWeather(UpdateGetFirst());
// exit the update thread
m_bThreadRunning = false;
}
/////////////////////////////////////////////////////////////////////////////////////////
// update all weather station
// AutoUpdate = true if it is from automatic update using timer
// false if it is from update by clicking the main menu
void CWeatherProto::UpdateAll(BOOL AutoUpdate, BOOL RemoveData)
{
// add all weather contact to the update queue list
for (auto &hContact : AccContacts())
if (!getByte(hContact, "AutoUpdate") || !AutoUpdate) {
if (RemoveData)
db_delete_module(hContact, WEATHERCONDITION);
UpdateListAdd(hContact);
}
// if it is not updating, then start the update thread process
// if it is updating, the stations just added to the queue will get updated by the already-running process
if (!m_bThreadRunning)
ForkThread(&CWeatherProto::UpdateThread);
}
/////////////////////////////////////////////////////////////////////////////////////////
// update a single station
// wParam = handle for the weather station that is going to be updated
INT_PTR CWeatherProto::UpdateSingleStation(WPARAM wParam, LPARAM)
{
if (IsMyContact(wParam)) {
// add the station to the end of the update queue
UpdateListAdd(wParam);
// if it is not updating, then start the update thread process
// if it is updating, the stations just added to the queue will get
// updated by the already-running process
if (!m_bThreadRunning)
ForkThread(&CWeatherProto::UpdateThread);
}
return 0;
}
/////////////////////////////////////////////////////////////////////////////////////////
// update a single station with removing the old data
// wParam = handle for the weather station that is going to be updated
INT_PTR CWeatherProto::UpdateSingleRemove(WPARAM hContact, LPARAM)
{
if (IsMyContact(hContact)) {
// add the station to the end of the update queue, and also remove old data
db_delete_module(hContact, WEATHERCONDITION);
UpdateListAdd(hContact);
// if it is not updating, then start the update thread process
// if it is updating, the stations just added to the queue will get updated by the already-running process
if (!m_bThreadRunning)
ForkThread(&CWeatherProto::UpdateThread);
}
return 0;
}
/////////////////////////////////////////////////////////////////////////////////////////
// the "Update All" menu item in main menu
INT_PTR CWeatherProto::UpdateAllInfo(WPARAM, LPARAM)
{
if (!m_bThreadRunning)
UpdateAll(FALSE, FALSE);
return 0;
}
/////////////////////////////////////////////////////////////////////////////////////////
// the "Update All" menu item in main menu and remove the old data
INT_PTR CWeatherProto::UpdateAllRemove(WPARAM, LPARAM)
{
if (!m_bThreadRunning)
UpdateAll(FALSE, TRUE);
return 0;
}
/////////////////////////////////////////////////////////////////////////////////////////
// getting weather data and save them into the database
// hContact = the contact to get the data
static wchar_t *moon2str(double phase)
{
if (phase < 0.05) return TranslateT("New moon");
if (phase < 0.26) return TranslateT("Waxing crescent");
if (phase < 0.51) return TranslateT("Waxing gibbous");
if (phase < 0.76) return TranslateT("Waning gibbous");
return TranslateT("Waning crescent");
}
static CMStringW parseConditions(const CMStringW &str)
{
CMStringW ret;
int iStart = 0;
while (true) {
auto substr = str.Tokenize(L",", iStart);
if (substr.IsEmpty())
break;
substr.Trim();
if (!ret.IsEmpty())
ret += ", ";
ret += TranslateW(substr);
}
return ret;
}
static double g_elevation = 0;
static void getData(OBJLIST &arValues, const JSONNode &node)
{
arValues.insert(new WIDATAITEM(LPGENW("Date"), L"", node["datetime"].as_mstring()));
arValues.insert(new WIDATAITEM(LPGENW("Condition"), L"", parseConditions(node["conditions"].as_mstring())));
arValues.insert(new WIDATAITEM(LPGENW("Temperature"), L"C", node["temp"].as_mstring()));
arValues.insert(new WIDATAITEM(LPGENW("High"), L"C", node["tempmax"].as_mstring()));
arValues.insert(new WIDATAITEM(LPGENW("Low"), L"C", node["tempmin"].as_mstring()));
CMStringW wszPressure(FORMAT, L"%lf", node["pressure"].as_float() - g_elevation);
arValues.insert(new WIDATAITEM(LPGENW("Pressure"), L"mb", wszPressure));
arValues.insert(new WIDATAITEM(LPGENW("Sunset"), L"", node["sunset"].as_mstring()));
arValues.insert(new WIDATAITEM(LPGENW("Sunrise"), L"", node["sunrise"].as_mstring()));
arValues.insert(new WIDATAITEM(LPGENW("Moon phase"), L"", moon2str(node["moonphase"].as_float())));
arValues.insert(new WIDATAITEM(LPGENW("Wind speed"), L"km/h", node["windspeed"].as_mstring()));
arValues.insert(new WIDATAITEM(LPGENW("Wind direction"), L"grad", node["winddir"].as_mstring()));
arValues.insert(new WIDATAITEM(LPGENW("Dew point"), L"C", node["dew"].as_mstring()));
arValues.insert(new WIDATAITEM(LPGENW("Visibility"), L"km", node["visibility"].as_mstring()));
arValues.insert(new WIDATAITEM(LPGENW("Humidity"), L"", node["humidity"].as_mstring()));
arValues.insert(new WIDATAITEM(LPGENW("Feel"), L"C", node["feelslike"].as_mstring()));
}
int CWeatherProto::GetWeatherData(MCONTACT hContact)
{
// get each part of the id's
CMStringW wszID(getMStringW(hContact, "ID"));
if (wszID.IsEmpty())
return INVALID_ID;
uint16_t cond = NA;
// download the html file from the internet
WeatherReply reply(RunQuery(wszID, 7));
if (!reply)
return reply.error();
auto &root = reply.data();
// writing current conditions
auto &curr = root["currentConditions"];
g_elevation = root["elevation"].as_float() / 7.877;
WIDATAITEMLIST arValues;
getData(arValues, curr);
auto szIcon = curr["icon"].as_string();
if (szIcon == "snow")
cond = SNOW;
else if (szIcon == "snow-showers-day" || szIcon == "snow-showers-night")
cond = SSHOWER;
else if (szIcon == "thunder" || szIcon == "thunder-showers-day" || szIcon == "thunder-showers-night")
cond = LIGHT;
else if (szIcon == "partly-cloudy-day" || szIcon == "partly-cloudy-night" || szIcon == "wind")
cond = PCLOUDY;
else if (szIcon == "fog")
cond = FOG;
else if (szIcon == "rain")
cond = RAIN;
else if (szIcon == "showers-day" || szIcon == "showers-night")
cond = RSHOWER;
else if (szIcon == "clear-day" || szIcon == "clear-night")
cond = SUNNY;
else if (szIcon == "rain")
cond = RAIN;
else if (szIcon == "cloudy")
cond = CLOUDY;
// writing forecast
db_set_ws(hContact, WEATHERCONDITION, "Update", curr["datetime"].as_mstring());
for (auto &it : arValues) {
ConvertDataValue(it);
if (!it->Value.IsEmpty())
db_set_ws(hContact, WEATHERCONDITION, _T2A(it->Name), it->Value);
}
int iFore = 0;
for (auto &fore : root["days"]) {
WIDATAITEMLIST arDaily;
getData(arDaily, fore);
CMStringW result;
for (auto &it : arDaily) {
ConvertDataValue(it);
if (it->Value.IsEmpty())
continue;
// insert missing values from day 0 into current
if (iFore == 0)
if (auto *pOld = arValues.Find(it->Name))
if (pOld->Value.IsEmpty() || pOld->Value == NODATA)
db_set_ws(hContact, WEATHERCONDITION, _T2A(it->Name), it->Value);
if (!result.IsEmpty())
result += L"; ";
result.AppendFormat(L"%s: %s", TranslateW(it->Name), it->Value.c_str());
}
CMStringA szSetting(FORMAT, "Forecast Day %d", iFore++);
db_set_ws(hContact, WEATHERCONDITION, szSetting, result);
arValues.destroy();
}
// assign condition icon
setWord(hContact, "StatusIcon", cond);
return 0;
}
/////////////////////////////////////////////////////////////////////////////////////////
static int enumSettings(const char *pszSetting, void *param)
{
auto *pList = (OBJLIST*)param;
if (!pList->find((char*)pszSetting))
pList->insert(newStr(pszSetting));
return 0;
}
void CWeatherProto::GetVarsDescr(CMStringW &wszDescr)
{
OBJLIST vars(10, strcmp);
for (int i = 1; i <= 7; i++)
vars.insert(newStr(CMStringA(FORMAT, "Forecast Day %d", i)));
for (auto &cc : AccContacts())
db_enum_settings(cc, &enumSettings, WEATHERCONDITION, &vars);
CMStringW str;
for (auto &it : vars) {
if (!str.IsEmpty())
str.Append(L", ");
str.AppendFormat(L"%%[%S]", it);
}
wszDescr += str;
}
/////////////////////////////////////////////////////////////////////////////////////////
// main auto-update timer
void CWeatherProto::DoUpdate()
{
// only run if it is not current updating and the auto update option is enabled
if (!m_bThreadRunning && opt.CAutoUpdate && !Miranda_IsTerminated() && m_iStatus == ID_STATUS_ONLINE)
UpdateAll(TRUE, FALSE);
}
// temporary timer for first run
// when this is run, it kill the old startup timer and create the permenant one above
void CWeatherProto::StartUpdate()
{
m_bThreadRunning = false;
if (!Miranda_IsTerminated())
m_impl.m_update.Start(opt.UpdateTime * 60000);
}
void CWeatherProto::RestartTimer()
{
m_impl.m_update.Stop();
m_impl.m_update.Start(opt.UpdateTime * 60000);
}