unit umainform; { Foobot Monitor Copyright (C)2016 Gordon Bamber minsadorada@charcodelvalle.com This project requires FPCv3.1.1+ and Lazarus V1.7+ This source 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; either version 2 of the License, or (at your option) any later version. This code 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. A copy of the GNU General Public License is available on the World Wide Web at . You can also obtain it by writing to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. REQUIREMENTS ============ * Custom sensors unit from Industrial package (foobot_sendors and foobot_sensors.res) * Custom httpdclient unit (foobot_httpclient) * CryptINI, TChart components * FPCv3.1.1+ and Lazarus V1.7+ * A connected Foobot VERSION HISTORY =============== V0.0.1.0: Initial commit V0.0.2.0: Trayicon added V0.0.3.0: Added Help menu. Updated Options menu V0.0.4.0: Graph added V0.1.0.0: Save/Load Alltime High/Lows. Reset values from menu V0.1.1.0: Save/Load Colours, Min and Max values to cfg file V0.2.1.0: Triggers,Multiple Foobots V0.2.2.0: Trigger config form, SaveLoad recommended values V0.2.4.0: Embedded help file added, replaced sensors.res with foobot_sensors.res V0.2.5.0: ?? } {$ifopt D+} {$DEFINE DEBUGMODE} // Debug mode does not load data from web {$ENDIF} {$mode objfpc}{$H+} interface uses // If Lazarus auto-inserts 'sensors' in the clause then delete it SysUtils, LazFileUtils, TAGraph, TAIntervalSources, TASeries, foobot_sensors, Forms, Controls, Graphics, Dialogs, ExtCtrls, StdCtrls, Menus, lclIntf, ComCtrls, foobot_utility, uCryptIni, ulazautoupdate, dateutils, uconfigform, utriggersform, Classes; const // Timer milliseconds ONEMINUTE = 60000; HALFHOUR = ONEMINUTE * 30; ONEHOUR = ONEMINUTE * 60; TWOHOURS = ONEHOUR * 2; FOURHOURS = ONEHOUR * 4; EIGHTHOURS = ONEHOUR * 8; TWENTYFOURHOURS = ONEHOUR * 24; // Colours for guages and graph lines COL_PM = 'clGreen'; COL_TMP = 'clRed'; COL_HUM = 'clMaroon'; COL_CO2 = 'clLime'; COL_VOC = 'clBlue'; COL_ALLPOLLU = 'clFuchsia'; // Sensor Gauge MINMAX Values MIN_PM = 0; MAX_PM = 1000; MIN_TMP = 0; MAX_TMP = 40; MIN_HUM = 10; MAX_HUM = 100; MIN_CO2 = 450; MAX_CO2 = 3000; MIN_VOC = 125; MAX_VOC = 1000; MIN_ALLPOLLU = 0; MAX_ALLPOLLU = 700; type { Tmainform } Tmainform = class(TForm) as_allpollu: TAnalogSensor; as_co2: TAnalogSensor; as_hum: TAnalogSensor; as_pm: TAnalogSensor; as_tmp: TAnalogSensor; as_voc: TAnalogSensor; Chart1: TChart; DateTimeIntervalChartSource1: TDateTimeIntervalChartSource; grp_health: TGroupBox; LazAutoUpdate1: TLazAutoUpdate; lbl_greenlighttmp: TLabel; lbl_greenlighthum: TLabel; lbl_greenlightco2: TLabel; lbl_greenlightvoc: TLabel; lbl_greenlightallpollu: TLabel; lbl_redlightpm: TLabel; lbl_redlighttmp: TLabel; lbl_redlighthum: TLabel; lbl_redlightco2: TLabel; lbl_redlightvoc: TLabel; lbl_redlightallpollu: TLabel; lbl_yellowlightpm: TLabel; lbl_greenlightpm: TLabel; lbl_yellowlighttmp: TLabel; lbl_yellowlighthum: TLabel; lbl_yellowlightco2: TLabel; lbl_yellowlightvoc: TLabel; lbl_yellowlightallpollu: TLabel; lineseries_allpollu: TLineSeries; lineseries_voc: TLineSeries; lineseries_co2: TLineSeries; lineseries_hum: TLineSeries; lineseries_tmp: TLineSeries; lineseries_pm: TLineSeries; grp_chart: TGroupBox; grp_pm: TGroupBox; grp_tmp: TGroupBox; grp_hum: TGroupBox; grp_co2: TGroupBox; grp_voc: TGroupBox; grp_allpollu: TGroupBox; grp_highlow: TGroupBox; grp_sensorDisplay: TGroupBox; lbl_pmhigh: TLabel; lbl_tmphigh: TLabel; lbl_humhigh: TLabel; lbl_co2high: TLabel; lbl_vochigh: TLabel; lbl_allpolluhigh: TLabel; lbl_pmlow: TLabel; lbl_tmplow: TLabel; lbl_humlow: TLabel; lbl_co2low: TLabel; lbl_voclow: TLabel; lbl_allpollulow: TLabel; MainMenu1: TMainMenu; mnu_helpCheckForUpdates: TMenuItem; mnu_helpHelpHTML: TMenuItem; mnu_helpFoobotAPIPage: TMenuItem; mnu_options_triggersActivateTriggers: TMenuItem; mnu_options_triggersSetTriggers: TMenuItem; mnu_optionsFoobotTriggers: TMenuItem; mnu_foobot: TMenuItem; mnu_optionsDisplayRedLines: TMenuItem; mnu_optionsDisplayYellowLines: TMenuItem; mnu_optionsDisplayGuagesOnly: TMenuItem; mnu_SampleEveryHalfHour: TMenuItem; mnu_optionsResetHighsLows: TMenuItem; mnu_optionsHomePage: TMenuItem; mnu_optionsSeperator1: TMenuItem; mnu_helpAbout: TMenuItem; mnu_help: TMenuItem; mnupopup_fileRestore: TMenuItem; mnu_pupupClose: TMenuItem; mnu_optionsMinimiseToTray: TMenuItem; mnu_optionsSaveHighLows: TMenuItem; mnu_SampleEvery24Hours: TMenuItem; mnu_SampleEvery8Hours: TMenuItem; mnu_SampleEvery4Hours: TMenuItem; mnu_SampleEvery2Hours: TMenuItem; mnu_SampleEvery1Hour: TMenuItem; mnu_optionsSampleEvery: TMenuItem; mnu_optionsTakeReadingNow: TMenuItem; mnu_optionsDisplay: TMenuItem; mnu_options: TMenuItem; mnu_fileExit: TMenuItem; mnu_file: TMenuItem; pnl_healthpm: TPanel; pnl_healthallpollu: TPanel; pnl_healthvoc: TPanel; pnl_healthco2: TPanel; pnl_healthhum: TPanel; pnl_healthtmp: TPanel; sls_pm: TStopLightSensor; sls_allpollu: TStopLightSensor; sls_voc: TStopLightSensor; sls_co2: TStopLightSensor; sls_hum: TStopLightSensor; sls_tmp: TStopLightSensor; sts: TStatusBar; traypopup: TPopupMenu; tmr_foobot: TTimer; TrayIcon1: TTrayIcon; procedure FormActivate(Sender: TObject); procedure FormClose(Sender: TObject; var CloseAction: TCloseAction); procedure FormCloseQuery(Sender: TObject; var CanClose: boolean); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure FormShow(Sender: TObject); procedure FormWindowStateChange(Sender: TObject); procedure mnupopup_fileRestoreClick(Sender: TObject); procedure mnu_fileExitClick(Sender: TObject); procedure mnu_helpAboutClick(Sender: TObject); procedure mnu_helpCheckForUpdatesClick(Sender: TObject); procedure mnu_helpFoobotAPIPageClick(Sender: TObject); procedure mnu_helpHelpHTMLClick(Sender: TObject); procedure mnu_optionsDisplayGuagesOnlyClick(Sender: TObject); procedure mnu_optionsDisplayRedLinesClick(Sender: TObject); procedure mnu_optionsDisplayYellowLinesClick(Sender: TObject); procedure mnu_optionsMinimiseToTrayClick(Sender: TObject); procedure mnu_optionsHomePageClick(Sender: TObject); procedure mnu_optionsResetHighsLowsClick(Sender: TObject); procedure mnu_optionsSaveHighLowsClick(Sender: TObject); procedure mnu_optionsTakeReadingNowClick(Sender: TObject); procedure mnu_options_triggersActivateTriggersClick(Sender: TObject); procedure mnu_options_triggersSetTriggersClick(Sender: TObject); procedure mnu_SampleEvery1HourClick(Sender: TObject); procedure mnu_SampleEvery24HoursClick(Sender: TObject); procedure mnu_SampleEvery2HoursClick(Sender: TObject); procedure mnu_SampleEvery4HoursClick(Sender: TObject); procedure mnu_SampleEvery8HoursClick(Sender: TObject); procedure mnu_SampleEveryHalfHourClick(Sender: TObject); procedure tmr_foobotTimer(Sender: TObject); procedure TrayIcon1Click(Sender: TObject); procedure ShowHintInStatusBar(Sender:TObject); private sSecretKey, sFoobotUserName, sUUID: string; bDisplayGuagesOnly, bDisplayYellowLines, bDisplayRedLines: boolean; iFudgeFactor: integer; HighTriggerColor, LowTriggerColor: TColor; foobotmenuarray: array of TMenuItem; sStandardHintText:String; procedure DisplayReadings; procedure UpdateGuage(Sender: TAnalogSensor; SensorNumber: integer); procedure UpdateHighLow(SensorNumber: integer); procedure GraphHistory; procedure GraphCurrentReading; procedure SetYellowRecommendedLevels; procedure SetRedSessionMax; procedure UpdateHealth; procedure PopulateFoobotMenu; procedure ChangeCurrentFoobot(Sender: TObject); procedure SaveConfig; procedure LoadConfig; procedure SetMinMaxTriggers; procedure SetTrafficLightStats(const iSensorNum: integer; const HIGHLOW: integer); procedure DoHighTriggerAlert(const iSensorNum: integer; const aValue: variant); procedure DoLowTriggerAlert(const iSensorNum: integer; const aValue: variant); procedure RestoreNormalColour(const iSensorNum: integer); procedure PopulateHints; // for i8n use later public iCurrentFoobot: integer; INI: TCryptINIfile; end; var mainform: Tmainform; // Used in extracting the help html file sHelpFilePath:String; S: TResourceStream; F: TFileStream; implementation uses uSplash; {$R *.lfm} { Tmainform } procedure Tmainform.PopulateHints; // ToDo: i8n begin // Traffic light controls sls_pm.Hint:='|Shows health of Particulates'; sls_tmp.Hint:='|Shows health of Temperature'; sls_hum.Hint:='|Shows health of Humidity'; sls_co2.Hint:='|Shows health of Carbon Dioxide'; sls_voc.Hint:='|Shows health of Volatile Organics'; sls_allpollu.Hint:='|Shows health of All Pollution'; // Traffic light labels lbl_greenlightpm.Hint:='|Shows green when below recommended value'; lbl_greenlighttmp.Hint:='|Shows green when below recommended value'; lbl_greenlighthum.Hint:='|Shows green when below recommended value'; lbl_greenlightco2.Hint:='|Shows green when below recommended value'; lbl_greenlightvoc.Hint:='|Shows green when below recommended value'; lbl_greenlightallpollu.Hint:='|Shows green wne below recommended value'; lbl_redlightpm.Hint:='|Shows red if High or Low value is triggered'; lbl_redlighttmp.Hint:='|Shows red if High or Low value is triggered'; lbl_redlighthum.Hint:='|Shows red if High or Low value is triggered'; lbl_redlightco2.Hint:='|Shows red if High or Low value is triggered'; lbl_redlightvoc.Hint:='|Shows red if High or Low value is triggered'; lbl_redlightallpollu.Hint:='|Shows red if High or Low value is triggered'; lbl_yellowlightpm.Hint:='|Shows yellow when above recommended value'; lbl_yellowlighttmp.Hint:='|Shows yellow when above recommended value'; lbl_yellowlighthum.Hint:='|Shows yellow when above recommended value'; lbl_yellowlightco2.Hint:='|Shows yellow when above recommended value'; lbl_yellowlightvoc.Hint:='|Shows yellow when above recommended value'; lbl_yellowlightallpollu.Hint:='|Shows yellow when above recommended value'; // All-time highs and lows lbl_pmhigh.Hint:='|All-time highest value recorded'; lbl_tmphigh.Hint:='|All-time highest value recorded'; lbl_humhigh.Hint:='|All-time highest value recorded'; lbl_co2high.Hint:='|All-time highest value recorded'; lbl_vochigh.Hint:='|All-time highest value recorded'; lbl_allpolluhigh.Hint:='|All-time highest value recorded'; lbl_pmlow.Hint:='|All-time lowest value recorded'; lbl_tmplow.Hint:='|All-time lowest value recorded'; lbl_humlow.Hint:='|All-time lowest value recorded'; lbl_co2low.Hint:='|All-time lowest value recorded'; lbl_voclow.Hint:='|All-time lowest value recorded'; lbl_allpollulow.Hint:='|All-time lowest value recorded'; // GroupBoxes grp_chart.Hint:='|' + sStandardHintText; grp_pm.Hint:='|' + sStandardHintText; grp_tmp.Hint:='|' + sStandardHintText; grp_hum.Hint:='|' + sStandardHintText; grp_co2.Hint:='|' + sStandardHintText; grp_voc.Hint:='|' + sStandardHintText; grp_allpollu.Hint:='|' + sStandardHintText; grp_highlow.Hint:='|' + sStandardHintText; grp_sensorDisplay.Hint:='|' + sStandardHintText; grp_health.Hint:='|' + sStandardHintText; // Sensors as_pm.Hint:='|Particulates level'; as_tmp.Hint:='|Temperature'; as_hum.Hint:='|Humidity'; as_co2.Hint:='|Co2 level'; as_voc.Hint:='|Volatile organics level'; as_allpollu.Hint:='|All pollution level'; Chart1.Hint:='|Rolling chart showing levels of all sensors'; end; procedure Tmainform.FormCreate(Sender: TObject); begin Caption := Application.Title; Icon := Application.Icon; // Crypted INI used INI := TCryptINIfile.Create(GetAppConfigFile(False)); if INI.IsVirgin then begin INI.WriteIdent('Gordon Bamber', '(c)2016', 'GPLV2', 'minesadorada@charcodelvalle.com', True); end; if not INI.VerifyIdent('41d10218d247980fc5e871b6b7844483') then begin ShowMessage(Application.Title + ' has been tampered wth. Please re-install from a trusted source.'); FreeAndNil(INI); Application.Terminate; end; INI.SectionHashing := False; ResetHighLows; // Read from ini file later iFudgeFactor := 20; // only needed if height set in form.create bDisplayGuagesOnly := False; INI.PlainTextMode := True; // Use CryptINI in legacy mode // Set defaults HighTriggerColor := clYellow; LowTriggerColor := clAqua; // Read config from INI file bDisplayYellowLines := INI.ReadBool('Config', 'DisplayYellowLines', False); mnu_optionsDisplayYellowLines.Checked := bDisplayYellowLines; bDisplayRedLines := INI.ReadBool('Config', 'DisplayRedLines', False); mnu_optionsDisplayRedLines.Checked := bDisplayRedLines; INI.PlainTextMode := False; // Use CryptINI in Encrypted mode SetYellowRecommendedLevels; SetRedSessionMax; // Set up System Tray TrayIcon1.Icon := Application.Icon; TrayIcon1.Hint := Application.Title; DateTimeIntervalChartSource1.DateTimeFormat := 'hh:nn'; LoadConfig; // Load from INI file {$IFDEF DEBUGMODE} UseTriggers := False; {$ENDIF} if UseTriggers then begin mnu_options_triggersActivateTriggers.Enabled := True; mnu_options_triggersActivateTriggers.Checked := True; mnu_options_triggersActivateTriggers.Caption := 'Set Triggers Off'; end; sStandardHintText:='Welcome to ' + Application.Title; PopulateHints; sts.SimpleText:=sStandardHintText; Application.OnHint := @ShowHintInStatusBar; end; procedure Tmainform.FormActivate(Sender: TObject); var sTempFoobotUserName, sTempSecretKey: string; iCount: integer; begin ClientHeight := grp_sensorDisplay.Height + grp_highlow.Height + grp_health.Height + grp_chart.Height; Application.ProcessMessages; // Allow user to enter values in INIFile sTempFoobotUserName := INI.ReadUnencryptedString('Config', 'Foobot User', 'unknown'); sTempSecretKey := INI.ReadUnencryptedString('Config', 'Secret Key', 'unknown'); if ((sTempFoobotUserName <> 'unknown') and (sTempSecretKey <> 'unknown')) then begin INI.WriteString('Foobot', 'Foobot User', sTempFoobotUserName); INI.DeleteKey('Config', 'Foobot User'); INI.WriteString('Foobot', 'Secret Key', sTempSecretKey); INI.DeleteKey('Config', 'Secret Key'); end; // Fetch Username and API_KEY sFoobotUserName := INI.ReadString('Foobot', 'Foobot User', 'unknown'); sSecretKey := INI.ReadString('Foobot', 'Secret Key', 'unknown'); if ((sFoobotUserName <> 'unknown') and (sSecretKey <> 'unknown')) then begin //Show; if FetchFoobotIdentity(sFoobotUserName, sSecretKey) then begin if FoobotIdentityObject.FoobotIdentityList.Count > 0 then begin sUUID := FoobotIdentityObject.FoobotIdentityList.Items[0].uuid; SaveLoadHighLows := INI.ReadBool('Foobot', 'SaveLoadHighLows', True); mnu_optionsSaveHighLows.Checked := SaveLoadHighLows; if SaveLoadHighLows then LoadHighLows; GraphHistory; // Switch off for testing tmr_foobot.Interval := ONEHOUR; {$IFNDEF DEBUGMODE} tmr_foobot.Enabled := True; {$ENDIF} // Everything OK - lets go! iCurrentFoobot := 0; PopulateFoobotMenu; LoadTriggers; // This can only be done if we have a Foobot Identity // as each Foobot has its own trigger values SetMinMaxTriggers; // Adjust if necesarry for preset Guage High/Low limits LoadRecommendedLevels; // into RecommendedLevelsArray for iCount := C_PM to C_ALLPOLLU do SetTrafficLightStats(iCount, C_HIGH); Show; {$IFNDEF DEBUGMODE} mnu_optionsTakeReadingNow.Click; {$ENDIF} grp_sensorDisplay.Refresh; grp_highlow.Refresh; grp_health.Refresh; Update; Application.ProcessMessages; splashform.hide; FreeAndNil(SplashForm); // Free memory used by SplashForm Application.ProcessMessages; end else begin // Identity.Count = 0 end; end else begin // Unable to fetch foobot identity ShowMessage('Cannot locate your Foobot. Click OK to close the application'); Close; end; end else begin // No valid cfg. Show config form Hide; splashform.Hide; FreeAndNil(SplashForm); // Free memory used by SplashForm Application.ProcessMessages; configform.ShowModal; // If user quit without data, then bail out if not configform.bValid then begin Close; end; // Store encrypted Username and API_KEY INI.WriteString('Foobot', 'Foobot User', configform.FoobotUsername); INI.WriteString('Foobot', 'Secret Key', configform.FoobotSecretKey); //sFoobotUserName := INI.ReadString('Foobot', 'Foobot User', 'unknown'); //sSecretKey := INI.ReadString('Foobot', 'Secret Key', 'unknown'); ShowMessage('Click OK to store settings and close the app.' + LineEnding + 'New settings are applied on resart.'); Close; end; end; procedure Tmainform.ChangeCurrentFoobot(Sender: TObject); // Called from 'Foobot' TSubmenuitem.click begin iCurrentFoobot := (Sender as TMenuItem).Tag; mnu_optionsTakeReadingNow.Click; // also triggers DoDisplay end; procedure Tmainform.SetTrafficLightStats(const iSensorNum: integer; const HIGHLOW: integer); // Called via a loop in form.create with HIGHLOW=C_HIGH (all traffic light captions set) // Called in DoHighTriggerAlert with HIGHLOW=C_HIGH (specific traffic light caption set) // Called in DoLowTriggerAlert with HIGHLOW=C_LOW (specific traffic light caption set) begin {$IFDEF DEBUGMODE}Exit;{$ENDIF} if iSensorNum = C_PM then if (HIGHLOW = C_HIGH) then lbl_redlightpm.Caption := Format('> %.1f %s', [double(FooBotTriggerArray[C_HIGH, C_PM]), FoobotDataObject.Units[C_PM]]) else lbl_redlightpm.Caption := Format('< %.1f %s', [double(FooBotTriggerArray[C_LOW, C_PM]), FoobotDataObject.Units[C_PM]]); lbl_yellowlightpm.Caption := Format('> %.1f %s', [RecommendedLevelsArray[C_PM], FoobotDataObject.Units[C_PM]]); lbl_greenlightpm.Caption := Format('< %.1f %s', [RecommendedLevelsArray[C_PM], FoobotDataObject.Units[C_PM]]); if iSensorNum = C_TMP then if (HIGHLOW = C_HIGH) then lbl_redlighttmp.Caption := Format('> %.1f %s', [double(FooBotTriggerArray[C_HIGH, C_TMP]), FoobotDataObject.Units[C_TMP]]) else lbl_redlighttmp.Caption := Format('< %.1f %s', [double(FooBotTriggerArray[C_LOW, C_TMP]), FoobotDataObject.Units[C_TMP]]); lbl_yellowlighttmp.Caption := Format('> %.1f %s', [RecommendedLevelsArray[C_TMP], FoobotDataObject.Units[C_TMP]]); lbl_greenlighttmp.Caption := Format('< %.1f %s', [RecommendedLevelsArray[C_TMP], FoobotDataObject.Units[C_TMP]]); if iSensorNum = C_HUM then if (HIGHLOW = C_HIGH) then lbl_redlighthum.Caption := Format('> %.1f %s', [double(FooBotTriggerArray[C_HIGH, C_HUM]), FoobotDataObject.Units[C_HUM]]) else lbl_redlighthum.Caption := Format('< %.1f %s', [double(FooBotTriggerArray[C_LOW, C_HUM]), FoobotDataObject.Units[C_HUM]]); lbl_yellowlighthum.Caption := Format('> %.1f %s', [RecommendedLevelsArray[C_HUM], FoobotDataObject.Units[C_HUM]]); lbl_greenlighthum.Caption := Format('< %.1f %s', [RecommendedLevelsArray[C_HUM], FoobotDataObject.Units[C_HUM]]); if iSensorNum = C_CO2 then if (HIGHLOW = C_HIGH) then lbl_redlightco2.Caption := Format('> %.0f %s', [double(FooBotTriggerArray[C_HIGH, C_CO2]), FoobotDataObject.Units[C_CO2]]) else lbl_redlightco2.Caption := Format('< %.0f %s', [double(FooBotTriggerArray[C_LOW, C_CO2]), FoobotDataObject.Units[C_CO2]]); lbl_yellowlightco2.Caption := Format('> %.0f %s', [RecommendedLevelsArray[C_CO2], FoobotDataObject.Units[C_CO2]]); lbl_greenlightco2.Caption := Format('< %.0f %s', [RecommendedLevelsArray[C_CO2], FoobotDataObject.Units[C_CO2]]); if iSensorNum = C_VOC then if (HIGHLOW = C_HIGH) then lbl_redlightvoc.Caption := Format('> %.0f %s', [double(FooBotTriggerArray[C_HIGH, C_VOC]), FoobotDataObject.Units[C_VOC]]) else lbl_redlightvoc.Caption := Format('< %.0f %s', [double(FooBotTriggerArray[C_LOW, C_VOC]), FoobotDataObject.Units[C_VOC]]); lbl_yellowlightvoc.Caption := Format('> %.0f %s', [RecommendedLevelsArray[C_VOC], FoobotDataObject.Units[C_VOC]]); lbl_greenlightvoc.Caption := Format('< %.0f %s', [RecommendedLevelsArray[C_VOC], FoobotDataObject.Units[C_VOC]]); if iSensorNum = C_ALLPOLLU then if (HIGHLOW = C_HIGH) then lbl_redlightallpollu.Caption := Format('> %.1f %s', [double(FooBotTriggerArray[C_HIGH, C_ALLPOLLU]), FoobotDataObject.Units[C_ALLPOLLU]]) else lbl_redlightallpollu.Caption := Format('< %.1f %s', [double(FooBotTriggerArray[C_LOW, C_ALLPOLLU]), FoobotDataObject.Units[C_ALLPOLLU]]); lbl_yellowlightallpollu.Caption := Format('> %.1f %s', [RecommendedLevelsArray[C_ALLPOLLU], FoobotDataObject.Units[C_ALLPOLLU]]); lbl_greenlightallpollu.Caption := Format('< %.1f %s', [RecommendedLevelsArray[C_ALLPOLLU], FoobotDataObject.Units[C_ALLPOLLU]]); grp_health.Refresh; end; procedure Tmainform.PopulateFoobotMenu; // Uses dynamic foobotmenuarray var iCount: integer; begin if (FoobotIdentityObject.FoobotIdentityList.Count = 0) then Exit; SetLength(foobotmenuarray, FoobotIdentityObject.FoobotIdentityList.Count); for iCount := 0 to Pred(FoobotIdentityObject.FoobotIdentityList.Count) do begin foobotmenuarray[iCount] := TMenuItem.Create(MainMenu1); with foobotmenuarray[iCount] do begin Caption := FoobotIdentityObject.FoobotIdentityList[iCount].Name; AutoCheck := True; RadioItem := True; OnClick := @ChangeCurrentFoobot; Tag := iCount; if iCount = 0 then Checked := True; end; end; mnu_foobot.Add(foobotmenuarray); end; procedure Tmainform.FormClose(Sender: TObject; var CloseAction: TCloseAction); begin SaveConfig; // to .cfg file if UseTriggers then SaveTriggers; // To .ini file SaveRecommendedLevels; CloseAction := caFree; end; procedure Tmainform.FormCloseQuery(Sender: TObject; var CanClose: boolean); begin If LazAutoUpdate1.DownloadInprogress then CanClose:=False; end; procedure Tmainform.FormDestroy(Sender: TObject); begin If Assigned (splashform) then FreeAndNil(splashform); FreeAndNil(INI); end; procedure Tmainform.FormShow(Sender: TObject); begin LazAutoUpdate1.ShowWhatsNewIfAvailable; end; procedure Tmainform.SaveConfig; // For all Foobots begin with INI do begin PlainTextMode := True; // Colours WriteString('Config', 'pmColour', ColorToString(as_pm.ColorFore)); WriteString('Config', 'tmpColour', ColorToString(as_tmp.ColorFore)); WriteString('Config', 'humColour', ColorToString(as_hum.ColorFore)); WriteString('Config', 'co2Colour', ColorToString(as_co2.ColorFore)); WriteString('Config', 'vocColour', ColorToString(as_voc.ColorFore)); WriteString('Config', 'allpolluColour', ColorToString(as_allpollu.ColorFore)); // Max and Min WriteFloat('Config', 'pmMinValue', as_pm.ValueMin); WriteFloat('Config', 'pmMaxValue', as_pm.ValueMax); WriteFloat('Config', 'tmpMinValue', as_tmp.ValueMin); WriteFloat('Config', 'tmpMaxValue', as_tmp.ValueMax); WriteFloat('Config', 'humMinValue', as_hum.ValueMin); WriteFloat('Config', 'humMaxValue', as_hum.ValueMax); WriteFloat('Config', 'co2MinValue', as_co2.ValueMin); WriteFloat('Config', 'co2MaxValue', as_co2.ValueMax); WriteFloat('Config', 'vocMinValue', as_voc.ValueMin); WriteFloat('Config', 'vocMaxValue', as_voc.ValueMax); WriteFloat('Config', 'allpolluMinValue', as_allpollu.ValueMin); WriteFloat('Config', 'allpolluMaxValue', as_allpollu.ValueMax); WriteBool('Config', 'DisplayYellowLines', bDisplayYellowLines); WriteBool('Config', 'DisplayRedLines', bDisplayRedLines); // Triggers WriteBool('Config', 'UseTriggers', UseTriggers); WriteString('Config', 'HighTriggerColour', ColorToString(HighTriggerColor)); WriteString('Config', 'LowTriggerColour', ColorToString(LowTriggerColor)); PlainTextMode := False; end; end; procedure Tmainform.SetMinMaxTriggers; // Ensure Triggers are in range of High & Low guage values begin if (UseTriggers = False) then exit; if as_pm.ValueMin > GetLowTrigger(C_PM) then SetLowTrigger(C_PM, as_pm.ValueMin); if as_pm.ValueMax < GetHighTrigger(C_PM) then SetHighTrigger(C_PM, as_pm.ValueMax); if as_tmp.ValueMin > GetLowTrigger(C_TMP) then SetLowTrigger(C_TMP, as_tmp.ValueMin); if as_tmp.ValueMax < GetHighTrigger(C_TMP) then SetHighTrigger(C_TMP, as_tmp.ValueMax); if as_hum.ValueMin > GetLowTrigger(C_HUM) then SetLowTrigger(C_HUM, as_hum.ValueMin); if as_hum.ValueMax < GetHighTrigger(C_HUM) then SetHighTrigger(C_HUM, as_hum.ValueMax); if as_co2.ValueMin > GetLowTrigger(C_CO2) then SetLowTrigger(C_CO2, as_co2.ValueMin); if as_co2.ValueMax < GetHighTrigger(C_CO2) then SetHighTrigger(C_CO2, as_co2.ValueMax); if as_voc.ValueMin > GetLowTrigger(C_VOC) then SetLowTrigger(C_VOC, as_voc.ValueMin); if as_voc.ValueMax < GetHighTrigger(C_VOC) then SetHighTrigger(C_VOC, as_voc.ValueMax); if as_allpollu.ValueMin > GetLowTrigger(C_ALLPOLLU) then SetLowTrigger(C_ALLPOLLU, as_allpollu.ValueMin); if as_allpollu.ValueMax < GetHighTrigger(C_ALLPOLLU) then SetHighTrigger(C_ALLPOLLU, as_allpollu.ValueMax); end; procedure Tmainform.LoadConfig; // For all Foobots begin INI.PlainTextMode := True; // Colours as_pm.ColorFore := StringToColor(INI.ReadString('Config', 'pmColour', COL_PM)); as_tmp.ColorFore := StringToColor(INI.ReadString('Config', 'tmpColour', COL_TMP)); as_hum.ColorFore := StringToColor(INI.ReadString('Config', 'humColour', COL_HUM)); as_co2.ColorFore := StringToColor(INI.ReadString('Config', 'co2Colour', COL_CO2)); as_voc.ColorFore := StringToColor(INI.ReadString('Config', 'vocColour', COL_VOC)); as_allpollu.ColorFore := StringToColor( INI.ReadString('Config', 'allpolluColour', COL_ALLPOLLU)); lineSeries_pm.SeriesColor := as_pm.ColorFore; lineSeries_tmp.SeriesColor := as_tmp.ColorFore; lineSeries_hum.SeriesColor := as_hum.ColorFore; lineSeries_co2.SeriesColor := as_co2.ColorFore; lineSeries_voc.SeriesColor := as_voc.ColorFore; lineSeries_allpollu.SeriesColor := as_allpollu.ColorFore; // Max and Min as_pm.ValueMin := INI.ReadFloat('Config', 'pmMinValue', MIN_PM); as_pm.ValueMax := INI.ReadFloat('Config', 'pmMaxValue', MAX_PM); as_tmp.ValueMin := INI.ReadFloat('Config', 'tmpMinValue', MIN_TMP); as_tmp.ValueMax := INI.ReadFloat('Config', 'tmpMaxValue', MAX_TMP); as_hum.ValueMin := INI.ReadFloat('Config', 'humMinValue', MIN_HUM); as_hum.ValueMax := INI.ReadFloat('Config', 'humMaxValue', MAX_HUM); as_co2.ValueMin := INI.ReadFloat('Config', 'co2MinValue', MIN_CO2); as_co2.ValueMax := INI.ReadFloat('Config', 'co2MaxValue', MAX_CO2); as_voc.ValueMin := INI.ReadFloat('Config', 'vocMinValue', MIN_VOC); as_voc.ValueMax := INI.ReadFloat('Config', 'vocMaxValue', MAX_VOC); as_allpollu.ValueMin := INI.ReadFloat('Config', 'allpolluMinValue', MIN_ALLPOLLU); as_allpollu.ValueMax := INI.ReadFloat('Config', 'allpolluMaxValue', MAX_ALLPOLLU); // Triggers UseTriggers := INI.ReadBool('Config', 'UseTriggers', False); HighTriggerColor := StringToColor(INI.ReadString('Config', 'HighTriggerColour', 'clYellow')); LowTriggerColor := StringToColor(INI.ReadString('Config', 'LowTriggerColour', 'clAqua')); INI.PlainTextMode := False; end; procedure Tmainform.FormWindowStateChange(Sender: TObject); // Systray routine begin if mainform.WindowState = wsMinimized then begin mainform.WindowState := wsNormal; mainform.Hide; mainform.ShowInTaskBar := stNever; end; end; procedure Tmainform.mnupopup_fileRestoreClick(Sender: TObject); begin mainform.Show; end; procedure Tmainform.mnu_fileExitClick(Sender: TObject); begin Close; end; procedure Tmainform.mnu_helpAboutClick(Sender: TObject); var s: string; begin s := Application.Title + LineEnding; s += 'Version: ' + INI.ReadUnencryptedString('ProgramInfo', IDENT_APPVERSION, '') + LineEnding + LineEnding; s += INI.ReadUnencryptedString('ProgramInfo', IDENT_COPYRIGHT, ''); s += ' by ' + INI.ReadUnencryptedString('ProgramInfo', IDENT_AUTHOR, '') + LineEnding; s += 'Licence: ' + INI.ReadUnencryptedString('ProgramInfo', IDENT_LICENSE, '') + LineEnding; s += 'Made with LCL v ' + INI.ReadUnencryptedString('ProgramInfo', IDENT_LCLVERSION, ''); s += ' FPC v ' + INI.ReadUnencryptedString('ProgramInfo', IDENT_FPCVERSION, '') + LineEnding; s += 'Compiled ' + INI.ReadUnencryptedString('ProgramInfo', IDENT_LASTCOMPILED, '') + LineEnding; s += ' for ' + INI.ReadUnencryptedString('ProgramInfo', IDENT_TARGET, ''); MessageDlg('About ' + Application.Title, s, mtInformation, [mbOK], 0); end; procedure Tmainform.mnu_helpCheckForUpdatesClick(Sender: TObject); begin LazAutoUpdate1.AutoUpdate; end; procedure Tmainform.mnu_helpFoobotAPIPageClick(Sender: TObject); begin OpenURL('http://api.foobot.io/apidoc/index.html'); end; procedure Tmainform.mnu_helpHelpHTMLClick(Sender: TObject); Var s:String; begin If FileExists(sHelpFilePath) then OpenURL('file://' + sHelpFilePath) else ShowMessageFmt('Sorry, the help file %s is missing',[sHelpFilePath]); end; procedure Tmainform.mnu_optionsDisplayGuagesOnlyClick(Sender: TObject); begin bDisplayGuagesOnly := mnu_optionsDisplayGuagesOnly.Checked; if bDisplayGuagesOnly then mainform.ClientHeight := grp_sensorDisplay.Height + grp_health.Height else mainform.ClientHeight := grp_sensorDisplay.Height + grp_health.Height + grp_chart.Height + grp_highlow.Height; end; procedure Tmainform.mnu_optionsDisplayRedLinesClick(Sender: TObject); begin bDisplayRedLines := mnu_optionsDisplayRedLines.Checked; SetRedSessionMax; end; procedure Tmainform.mnu_optionsDisplayYellowLinesClick(Sender: TObject); begin bDisplayYellowLines := mnu_optionsDisplayYellowLines.Checked; SetYellowRecommendedLevels; end; procedure Tmainform.mnu_optionsMinimiseToTrayClick(Sender: TObject); begin mainform.WindowState := wsMinimized; mainform.FormWindowStateChange(Self); end; procedure Tmainform.mnu_optionsHomePageClick(Sender: TObject); begin OpenURL('http://wiki.freepascal.org/Foobot'); end; procedure Tmainform.mnu_optionsResetHighsLowsClick(Sender: TObject); var iCount: integer; begin if MessageDlg('This will erase the all-time high/low data permanently. Are you sure?', mtConfirmation, [mbYes, mbCancel], 0, mbCancel) = mrCancel then exit; ResetHighLows; SaveHighLows; for iCount := C_PM to C_ALLPOLLU do UpdateHighLow(iCount); end; procedure Tmainform.mnu_optionsSaveHighLowsClick(Sender: TObject); begin SaveLoadHighLows := mnu_optionsSaveHighLows.Checked; INI.WriteBool('Foobot', 'SaveLoadHighLows', SaveLoadHighLows); end; procedure Tmainform.mnu_optionsTakeReadingNowClick(Sender: TObject); begin mainform.Cursor := crHourGlass; if FetchFoobotData(dfLast, iCurrentFoobot, 0, 0, 0, 0, sSecretKey) then DisplayReadings else ShowMessage('Sorry - no readings available'); mainform.Cursor := crDefault; end; procedure Tmainform.mnu_options_triggersActivateTriggersClick(Sender: TObject); var icount: integer; begin mnu_options_triggersActivateTriggers.Checked := not mnu_options_triggersActivateTriggers.Checked; UseTriggers := mnu_options_triggersActivateTriggers.Checked; if UseTriggers then begin mnu_options_triggersActivateTriggers.Caption := 'Set Triggers Off'; LoadTriggers; SetMinMaxTriggers; DisplayReadings; end else begin mnu_options_triggersActivateTriggers.Caption := 'Set Triggers On'; for iCount := C_PM to C_ALLPOLLU do RestoreNormalColour(iCount); end; end; procedure Tmainform.mnu_options_triggersSetTriggersClick(Sender: TObject); Var iCount:Integer; begin If FoobotIdentityObject.FoobotIdentityList.Count = 0 then Exit;; triggersform.ShowModal; // If cancel was clicked nothing was changed mnu_options_triggersActivateTriggers.Enabled := True; LoadTriggers; // This can only be done if we have a Foobot Identity // as each Foobot has its own trigger values SetMinMaxTriggers; // Adjust if necesarry for preset Guage High/Low limits // LoadRecommendedLevels; // into RecommendedLevelsArray for iCount := C_PM to C_ALLPOLLU do SetTrafficLightStats(iCount, C_HIGH); UpdateHealth; DisplayReadings; Update; { ShowMessageFmt('Current high trigger: %.1f %s', [double(FooBotTriggerArray[C_HIGH, C_TMP]), FoobotDataObject.Units[C_TMP]]); } end; procedure Tmainform.mnu_SampleEveryHalfHourClick(Sender: TObject); begin tmr_foobot.Enabled := False; tmr_foobot.Interval := HALFHOUR; tmr_foobot.Enabled := True; end; procedure Tmainform.mnu_SampleEvery1HourClick(Sender: TObject); begin tmr_foobot.Enabled := False; tmr_foobot.Interval := ONEHOUR; tmr_foobot.Enabled := True; end; procedure Tmainform.mnu_SampleEvery24HoursClick(Sender: TObject); begin tmr_foobot.Enabled := False; tmr_foobot.Interval := TWENTYFOURHOURS; tmr_foobot.Enabled := True; end; procedure Tmainform.mnu_SampleEvery2HoursClick(Sender: TObject); begin tmr_foobot.Enabled := False; tmr_foobot.Interval := TWOHOURS; tmr_foobot.Enabled := True; end; procedure Tmainform.mnu_SampleEvery4HoursClick(Sender: TObject); begin tmr_foobot.Enabled := False; tmr_foobot.Interval := FOURHOURS; tmr_foobot.Enabled := True; end; procedure Tmainform.mnu_SampleEvery8HoursClick(Sender: TObject); begin tmr_foobot.Enabled := False; tmr_foobot.Interval := EIGHTHOURS; tmr_foobot.Enabled := True; end; procedure Tmainform.tmr_foobotTimer(Sender: TObject); begin if FetchFoobotData(dfLast, iCurrentFoobot, 0, 0, 0, 0, sSecretKey) then DisplayReadings else mainform.Caption := Format('Foobot "%s" - Read failure on %s', [FoobotIdentityObject.FoobotIdentityList[iCurrentFoobot].Name, FormatDateTime('dd/mm/yyyy - tt', Now)]); end; procedure Tmainform.TrayIcon1Click(Sender: TObject); begin mainform.Show; end; procedure Tmainform.SetRedSessionMax; // Sets red lines on guages begin if bDisplayRedLines = True then begin with as_pm do if Value > ValueRed then ValueRed := Value; with as_tmp do if Value > ValueRed then ValueRed := Value; with as_hum do if Value > ValueRed then ValueRed := Value; with as_co2 do if Value > ValueRed then ValueRed := Value; with as_voc do if Value > ValueRed then ValueRed := Value; with as_allpollu do if Value > ValueRed then ValueRed := Value; end else begin as_pm.ValueRed := as_pm.ValueMin; as_tmp.ValueRed := as_tmp.ValueMin; as_hum.ValueRed := as_hum.ValueMin; as_co2.ValueRed := as_co2.ValueMin; as_voc.ValueRed := as_voc.ValueMin; as_allpollu.ValueRed := as_allpollu.ValueMin; end; end; procedure Tmainform.SetYellowRecommendedLevels; // Sets yellow lines on guages begin if bDisplayYellowLines = True then begin as_pm.ValueYellow := RecommendedLevelsArray[C_PM]; as_tmp.ValueYellow := RecommendedLevelsArray[C_TMP]; as_hum.ValueYellow := RecommendedLevelsArray[C_HUM]; as_co2.ValueYellow := RecommendedLevelsArray[C_CO2]; as_voc.ValueYellow := RecommendedLevelsArray[C_VOC]; as_allpollu.ValueYellow := RecommendedLevelsArray[C_ALLPOLLU]; end else begin as_pm.ValueYellow := as_pm.ValueMin; as_tmp.ValueYellow := as_tmp.ValueMin; as_hum.ValueYellow := as_hum.ValueMin; as_co2.ValueYellow := as_co2.ValueMin; as_voc.ValueYellow := as_voc.ValueMin; as_allpollu.ValueYellow := as_allpollu.ValueMin; end; end; procedure Tmainform.UpdateHighLow(SensorNumber: integer); begin case SensorNumber of C_PM: begin lbl_pmhigh.Caption := Format('High: %f %s', [double(FoobotDataHighs[SensorNumber]), FoobotDataObject.Units[SensorNumber]]) + LineEnding + 'on ' + FormatDateTime('dd/mm tt', TDateTime(FoobotDataHighTimes[SensorNumber])); lbl_pmLow.Caption := Format('Low: %f %s', [double(FoobotDataLows[SensorNumber]), FoobotDataObject.Units[SensorNumber]]) + LineEnding + 'on ' + FormatDateTime('dd/mm tt', TDateTime( FoobotDataLowTimes[SensorNumber])); end; C_TMP: begin lbl_tmphigh.Caption := Format('High: %f %s', [double(FoobotDataHighs[SensorNumber]), FoobotDataObject.Units[SensorNumber]]) + LineEnding + 'on ' + FormatDateTime('dd/mm tt', TDateTime(FoobotDataHighTimes[SensorNumber])); lbl_tmpLow.Caption := Format('Low: %f %s', [double(FoobotDataLows[SensorNumber]), FoobotDataObject.Units[SensorNumber]]) + LineEnding + 'on ' + FormatDateTime('dd/mm tt', TDateTime( FoobotDataLowTimes[SensorNumber])); end; C_HUM: begin lbl_humhigh.Caption := Format('High: %f %s', [double(FoobotDataHighs[SensorNumber]), FoobotDataObject.Units[SensorNumber]]) + LineEnding + 'on ' + FormatDateTime('dd/mm tt', TDateTime(FoobotDataHighTimes[SensorNumber])); lbl_humLow.Caption := Format('Low: %f %s', [double(FoobotDataLows[SensorNumber]), FoobotDataObject.Units[SensorNumber]]) + LineEnding + 'on ' + FormatDateTime('dd/mm tt', TDateTime( FoobotDataLowTimes[SensorNumber])); end; C_CO2: begin lbl_co2high.Caption := Format('High: %d %s', [integer(FoobotDataHighs[SensorNumber]), FoobotDataObject.Units[SensorNumber]]) + LineEnding + 'on ' + FormatDateTime('dd/mm tt', TDateTime(FoobotDataHighTimes[SensorNumber])); lbl_co2Low.Caption := Format('Low: %d %s', [integer(FoobotDataLows[SensorNumber]), FoobotDataObject.Units[SensorNumber]]) + LineEnding + 'on ' + FormatDateTime('dd/mm tt', TDateTime( FoobotDataLowTimes[SensorNumber])); end; C_VOC: begin lbl_vochigh.Caption := Format('High: %d %s', [integer(FoobotDataHighs[SensorNumber]), FoobotDataObject.Units[SensorNumber]]) + LineEnding + 'on ' + FormatDateTime('dd/mm tt', TDateTime(FoobotDataHighTimes[SensorNumber])); lbl_vocLow.Caption := Format('Low: %d %s', [integer(FoobotDataLows[SensorNumber]), FoobotDataObject.Units[SensorNumber]]) + LineEnding + 'on ' + FormatDateTime('dd/mm tt', TDateTime( FoobotDataLowTimes[SensorNumber])); end; C_ALLPOLLU: begin lbl_allpolluhigh.Caption := Format('High: %f %s', [double(FoobotDataHighs[SensorNumber]), FoobotDataObject.Units[SensorNumber]]) + LineEnding + 'on ' + FormatDateTime('dd/mm tt', TDateTime(FoobotDataHighTimes[SensorNumber])); lbl_allpollulow.Caption := Format('Low: %f %s', [double(FoobotDataLows[SensorNumber]), FoobotDataObject.Units[SensorNumber]]) + LineEnding + 'on ' + FormatDateTime('dd/mm tt', TDateTime(FoobotDataLowTimes[SensorNumber])); end; else Exception.Create('Error in UpdateHighLow Case statement'); end; end; procedure Tmainform.UpdateGuage(Sender: TAnalogSensor; SensorNumber: integer); begin with Sender do begin case SensorNumber of C_PM: begin Value := FoobotData_pm[iCurrentFoobot]; Caption := Format('PM (%s): ', [FoobotDataObject.Units[SensorNumber]]); end; C_TMP: begin Value := FoobotData_tmp[iCurrentFoobot]; Caption := Format('Temp (%s): ', [FoobotDataObject.Units[SensorNumber]]); end; C_HUM: begin Value := FoobotData_hum[iCurrentFoobot]; Caption := Format('Hum. (%s): ', [FoobotDataObject.Units[SensorNumber]]); end; C_CO2: begin Value := FoobotData_co2[iCurrentFoobot]; Caption := Format('CO2 (%s): ', [FoobotDataObject.Units[SensorNumber]]); end; C_VOC: begin Value := FoobotData_voc[iCurrentFoobot]; Caption := Format('VOC (%s): ', [FoobotDataObject.Units[SensorNumber]]); end; C_ALLPOLLU: begin Value := FoobotData_allpollu[iCurrentFoobot]; Caption := Format('All (%s): ', [FoobotDataObject.Units[SensorNumber]]); end; end; if Value > ValueMax then ValueMax := Value; if bDisplayRedLines then if Value > ValueRed then ValueRed := Value; if bDisplayYellowLines then SetYellowRecommendedLevels; end; end; procedure Tmainform.DoHighTriggerAlert(const iSensorNum: integer; const aValue: variant); begin case iSensorNum of C_PM: as_pm.Color := HighTriggerColor; C_TMP: as_tmp.Color := HighTriggerColor; C_HUM: as_hum.Color := HighTriggerColor; C_CO2: as_co2.Color := HighTriggerColor; C_VOC: as_voc.Color := HighTriggerColor; C_ALLPOLLU: as_allpollu.Color := HighTriggerColor; end; SetTrafficLightStats(iSensorNum, C_HIGH); end; procedure Tmainform.DoLowTriggerAlert(const iSensorNum: integer; const aValue: variant); begin case iSensorNum of C_PM: as_pm.Color := LowTriggerColor; C_TMP: as_tmp.Color := LowTriggerColor; C_HUM: as_hum.Color := LowTriggerColor; C_CO2: as_co2.Color := LowTriggerColor; C_VOC: as_voc.Color := LowTriggerColor; C_ALLPOLLU: as_allpollu.Color := LowTriggerColor; end; SetTrafficLightStats(iSensorNum, C_LOW); end; procedure Tmainform.RestoreNormalColour(const iSensorNum: integer); begin case iSensorNum of C_PM: as_pm.Color := clDefault; C_TMP: as_tmp.Color := clDefault; C_HUM: as_hum.Color := clDefault; C_CO2: as_co2.Color := clDefault; C_VOC: as_voc.Color := clDefault; C_ALLPOLLU: as_allpollu.Color := clDefault; end; end; procedure Tmainform.UpdateHealth; begin if (as_pm.Value >= RecommendedLevelsArray[C_PM]) then sls_pm.State := slYELLOW else sls_pm.State := slGREEN; if (as_tmp.Value >= RecommendedLevelsArray[C_TMP]) then sls_tmp.State := slYELLOW else sls_tmp.State := slGREEN; if (as_hum.Value >= RecommendedLevelsArray[C_HUM]) then sls_hum.State := slYELLOW else sls_hum.State := slGREEN; if (as_co2.Value >= RecommendedLevelsArray[C_CO2]) then sls_co2.State := slYELLOW else sls_co2.State := slGREEN; if (as_voc.Value >= RecommendedLevelsArray[C_VOC]) then sls_voc.State := slYELLOW else sls_voc.State := slGREEN; if (as_allpollu.Value >= RecommendedLevelsArray[C_ALLPOLLU]) then sls_allpollu.State := slYELLOW else sls_allpollu.State := slGREEN; end; procedure Tmainform.DisplayReadings; var iCount: integer; begin if FoobotDataObjectToArrays = True then begin mainform.Caption := Format('Foobot "%s" - Last reading: ', [FoobotIdentityObject.FoobotIdentityList[iCurrentFoobot].Name]) + FormatDateTime('dd/mm/yyyy - tt', FoobotData_time[0]); UpdateGuage(as_pm, C_PM); UpdateGuage(as_tmp, C_TMP); UpdateGuage(as_hum, C_HUM); UpdateGuage(as_co2, C_CO2); UpdateGuage(as_voc, C_VOC); UpdateGuage(as_allpollu, C_ALLPOLLU); UpdateHealth; // Process Trigger Alerts on each call to FoobotDataObjectToArrays if UseTriggers then try // Look for alerts in each sensor for iCount := C_PM to C_ALLPOLLU do begin if (AlertRec[iCount].AlertTriggered = True) then begin case iCount of C_PM: sls_pm.State := slRED; C_TMP: sls_tmp.State := slRED; C_HUM: sls_hum.State := slRED; C_CO2: sls_co2.State := slRED; C_VOC: sls_voc.State := slRED; C_ALLPOLLU: sls_allpollu.State := slRED; end; // Alert found. High or low? if (AlertRec[iCount].AlertType = C_HIGH) then begin // A high alert - do something DoHighTriggerAlert(iCount, AlertRec[iCount].AlertValue); end else begin // A low alert - do something DoLowTriggerAlert(iCount, AlertRec[iCount].AlertValue); end; end else RestoreNormalColour(iCount); end; except raise Exception.Create('Unable to process triggers in DisplayReadings'); end; if not bDisplayGuagesOnly then begin for iCount := 1 to 6 do UpdateHighLow(iCount); end; GraphCurrentReading; end else raise Exception.Create('FoobotDataObjectToArrays error in DisplayReadings'); end; function AsPercent(aValue, aMin, aMax: double): double; begin if aMax > 0 then Result := (aValue / (aMax - aMin) * 100) else Result := 0; end; procedure Tmainform.GraphCurrentReading; begin {$IFDEF DEBUGMODE} Exit; {$ENDIF} try lineseries_pm.AddXY(FoobotData_time[iCurrentFoobot], AsPercent(FoobotData_pm[iCurrentFoobot], as_pm.ValueMin, as_pm.ValueMax)); lineseries_tmp.AddXY(FoobotData_time[iCurrentFoobot], AsPercent(FoobotData_tmp[iCurrentFoobot], as_tmp.ValueMin, as_tmp.ValueMax)); lineseries_hum.AddXY(FoobotData_time[iCurrentFoobot], AsPercent(FoobotData_hum[iCurrentFoobot], as_hum.ValueMin, as_hum.ValueMax)); lineseries_co2.AddXY(FoobotData_time[iCurrentFoobot], AsPercent(FoobotData_co2[iCurrentFoobot], as_co2.ValueMin, as_co2.ValueMax)); lineseries_voc.AddXY(FoobotData_time[iCurrentFoobot], AsPercent(FoobotData_voc[iCurrentFoobot], as_voc.ValueMin, as_voc.ValueMax)); lineseries_allpollu.AddXY(FoobotData_time[iCurrentFoobot], AsPercent(FoobotData_allpollu[iCurrentFoobot], as_allpollu.ValueMin, as_allpollu.ValueMax)); except raise Exception.Create('Unable to update graph in GraphCurrentReading'); end; end; procedure Tmainform.GraphHistory; // Fetch Hourly readings for the previous 2 days (AverageBy=3600) // Populate FoobotDataObjectToArrays var iCount: integer; iStartSeconds, iEndSeconds: int64; bTempUseTriggers: boolean; begin {$IFDEF DEBUGMODE} Exit; {$ENDIF} // Turn off triggers (if on) bTempUseTriggers := UseTriggers; UseTriggers := False; iEndSeconds := DateTimeToUnix(Now) - 3600; iStartSeconds := iEndSeconds - (2 * (24 * 3600)); // 49 hours before Now grp_chart.Caption := Format('History since %s', [FormatDateTime('dd/mm/yyyy hh:nn', UnixToDateTime(iStartSeconds))]); if FetchFoobotData(dfStartEnd, iCurrentFoobot, 0, 3600, iStartSeconds, iEndSeconds, sSecretKey) = False then exit; try if FoobotDataObjectToArrays then for iCount := 0 to Pred(High(FoobotData_time)) do begin lineseries_pm.AddXY(FoobotData_time[iCount], AsPercent(FoobotData_pm[iCount], as_pm.ValueMin, as_pm.ValueMax)); lineseries_tmp.AddXY(FoobotData_time[iCount], AsPercent(FoobotData_tmp[iCount], as_tmp.ValueMin, as_tmp.ValueMax)); lineseries_hum.AddXY(FoobotData_time[iCount], AsPercent(FoobotData_hum[iCount], as_hum.ValueMin, as_hum.ValueMax)); lineseries_co2.AddXY(FoobotData_time[iCount], AsPercent(FoobotData_co2[iCount], as_co2.ValueMin, as_co2.ValueMax)); lineseries_voc.AddXY(FoobotData_time[iCount], AsPercent(FoobotData_voc[iCount], as_voc.ValueMin, as_voc.ValueMax)); lineseries_allpollu.AddXY(FoobotData_time[iCount], AsPercent(FoobotData_allpollu[iCount], as_allpollu.ValueMin, as_allpollu.ValueMax)); end; finally UseTriggers := bTempUseTriggers; ResetArrays; // at end end; end; procedure Tmainform.ShowHintInStatusBar(Sender:TObject); begin sts.SimpleText := GetLongHint(Application.Hint); end; initialization //sHelpFilePath:=AppendPathDelim(GetCurrentDir) + 'foobotmonitorhelp.htm'; sHelpFilePath:=AppendPathDelim(GetAppConfigDir(false)) + 'foobotmonitorhelp.htm'; //sHelpFilePath:=ExtractFileDir(GetAppConfigFile(False)); //sHelpFilePath:=AppendPathDelim(sHelpFilePath) + 'foobotmonitorhelp.htm'; // This uses a resource file added via Project/Options (Laz 1.7+) if not FileExistsUTF8(sHelpFilePath) then begin // create a resource stream which points to the help file S := TResourceStream.Create(HInstance, 'FOOBOTMONITORHELP', MakeIntResource(10)); TRY try ForceDirectoriesUTF8(GetAppConfigDir(false)); F := TFileStream.Create(sHelpFilePath, fmCreate); try F.CopyFrom(S, S.Size); // copy data from the resource stream to file stream finally F.Free; // destroy the file stream end; finally S.Free; // destroy the resource stream end; EXCEPT raise Exception.Create('Could not create ' + sHelpFilePath); end; end; end.