unit IndoorInstruments;

  interface
    uses
      Classes       (**),
      Controls      (**),
      Forms         (* tForm *),
      HC3DM         (* avGCFridge, Global, qGCFridgeSettingx10,
                         SillyIntegerValue, SillyRealValue *),
      ObservedAndWM (* DHT20FailureReasons, tFailureReason, frDHT22ReadFailure,
                         frImplausibleRH, frImplausibleTemp, frSCD30SetAmbient,
                         frSCD30DataNotReady, frSCD40BadCO2,
                         frSCD40measureSingleShot, frSCD40readMeasurement,
                         frSCD40SetAmbient, GetObservedName, obGCFridge,
                         obGuestRoom, obKitchen, obLR, obMBR, tObserved,
                         obLeftPower, obOffice, obRightPower, ObservedInfo,
                         obShop, obTVRoom, SPS30FailureReasons, tTableUsed,
                         tuPower, tuTRHP *),
      StdCtrls      (* tLabel *),
      Windows       (**);

    type
      tfmIndoorSensors = class(tForm)
          lbAC:              tLabel;
          lbACOnOff:         tLabel;
          lbFridgeHerald:    tLabel;
          lbFurnace:         tLabel;
          lbFurnaceOnOff:    tLabel;
          lbGCFridge:        tLabel;
          lbHeaderMisc:      tLabel;
          lbHeaderOnOff:     tLabel;
          lbHeaderTempRH:    tLabel;
          lbKitchen:         tLabel;
          lbKitchenPM25:     tLabel;
          lbKitchenTRH:      tLabel;
          lbLR:              tLabel;
          lbLRLightAndLamp:  tLabel;
          lbKitchen2:        tLabel;
          lbLRTRH:           tLabel;
          lbMBR:             tLabel;
          lbMBRTRH:          tLabel;
          lbOffice:          tLabel;
          lbOfficeTRH:       tLabel;
          lbOther:           tLabel;
          lbOtherTRH:        tLabel;
          lbPower:           tLabel;
          lbShop:            tLabel;
          lbShopHeater:      tLabel;
          lbShopHeaterOnOff: tLabel;
          lbShopTRH:         tLabel;

          procedure FormDblClick(Sender: tObject);
          procedure FormShow    (Sender: tObject);
        private
          SCD30NotReady: array[tObserved] of integer;
          procedure LRLightAndLamp;
        public
          procedure BadData(const Reason: tFailureReason);
          procedure Initialize;
          procedure NewDataGeneric(const TU: tTableUsed);
          procedure NewDataLRLight(const LightLevel: real); // called from table_light
          procedure NewDataOnOff(const ObservedUID: tObserved;
                                 const AnchorLocal: tDateTime;
                                 const IsNowOn: boolean;
                                 const HoursPassed, HoursOn: real);
        end (* tfmIndoorSensors *);

    var
      fmIndoorSensors: tfmIndoorSensors;

  implementation
    uses
      DashBoard     (* fmDashboard *),
      Dialogs       (* ShowMessage, ShowMessageFmt *),
      eFrames       (* eFrameReal *),
      Graphics      (* clPurple, clWindowText, fsUnderline *),
      HC4Main       (* fmHC4Main *),
      HC3Utilities  (* OnOffDisplay, INILoadFloat, SPS30FailureString *),
      LabelAdjuster (* LeftMonitorScale, ms175, TheLabelAdjuster *),
      MyConsts      (* UsualFormColor *),
      SysUtils      (* Format, FormatDateTime *);
      {$R *.dfm}
                    
    var
      LRLampNowOn, LRLampStatusKnown: boolean;
      FridgeTooCold, FridgeTooHot, LeftAmps, LRLightLevel, RightAmps: real;
      RoomCO2: array[tObserved] of integer;

    procedure tfmIndoorSensors.BadData(const Reason: tFailureReason);
      (* note in UI *)

      var
        Explanation, ObservedString, TimeString: string;

      procedure BadLabel(TheLabel: tLabel);
        (* cliche *)

        begin (* BadLabel *)
          with TheLabel, Global.LatestData do
            begin (* TheLabel *)
              Caption := Format('%s as of %s', [Explanation, TimeString]);
              Font.Color := clPurple;
              Enabled := true
            end   (* TheLabel *)
        end   (* BadLabel *);

      begin (* tfmIndoorSensors.BadData *)
        ObservedString := GetObservedName(Global.LatestData.ObservedUID);
        TimeString := FormatDateTime('hhnn', Global.LatestData.LocalStamp);

        with Global.LatestData do
          begin (* Global.LatestData *)
            if Reason in DHT20FailureReasons then
              Explanation := DHT20FailureString(Reason)
            else if Reason in SPS30FailureReasons then
              Explanation := SPS30FailureString(Reason)
            else
              case Reason of
                      frDHT22ReadFailure: Explanation := 'DHT22 read failure'              ;
                         frImplausibleRH: Explanation := 'implausible RH'                  ;
                       frImplausibleTemp: Explanation := 'implausible temperature'         ;
                       frSCD30SetAmbient: Explanation := 'couldn''t set SCD30 pressure'    ;
                     frSCD30DataNotReady: begin (* SCD30 not ready *)
                                            inc(SCD30NotReady[ObservedUID]);
                                            Explanation := Format('SCD30 not ready (#%d)',
                                                                  [SCD30NotReady[ObservedUID]])
                                          end   (* SCD30 not ready *)                      ;
                           frSCD40BadCO2: Explanation := 'implausible CO2ppm'              ;
                frSCD40measureSingleShot: Explanation := 'couldn''t get SCD40 measurement' ;
                  frSCD40readMeasurement: Explanation := 'couldn''t read SCD40 measurement';
                       frSCD40SetAmbient: Explanation := 'couldn''t set SCD40 pressure'
                end (* case *);
            
            case ObservedUID of
                obKitchen: BadLabel(lbKitchenTRH);
                     obLR: BadLabel(lbLRTRH);
                    obMBR: BadLabel(lbMBRTRH);
                 obOffice: BadLabel(lbOfficeTRH);
                   obShop: BadLabel(lbShopTRH);
              obGuestRoom,
                 obTVRoom: begin (* unusual case *)
                             with lbOther do
                               Caption := ObservedString;
                             BadLabel(lbOtherTRH)
                           end   (* unusual case *)
              end (* case *)
          end   (* Global.LatestData *)
      end   (* tfmIndoorSensors.BadData *);

    procedure tfmIndoorSensors.FormDblClick(Sender: tObject);
      (* show master form *)

      begin (* tfmIndoorSensors.FormDblClick *)
        fmHC4Main.Show
      end   (* tfmIndoorSensors.FormDblClick *);

    procedure tfmIndoorSensors.FormShow(Sender: tObject);
      (* clean up appearance.  beyond font sizes, need linespacing fix and
      panel resizing *)

      const
        BottomMargin = 45;
        
      var
        LastBottom, PowerSimple: integer;
        ObservedUID: tObserved;

      begin (* tfmIndoorSensors.FormShow *)
        self.Color := UsualFormColor;
        PowerSimple   := 77;
        FridgeTooCold := INILoadFloat('garbage-closet fridge', 'FridgeTooCold', 35);
        FridgeTooHot  := INILoadFloat(                         'FridgeTooHot',  39);
        for ObservedUID := low(tObserved) to high(tObserved) do
          SCD30NotReady[ObservedUID] := 0;
        with TheLabelAdjuster do
          begin (* TheLabelAdjuster *)
            NextTop := 4;
            case LeftMonitorScale of
              ms150,
              ms175,
              ms200: PowerSimple := 77
              end (* case *);

            PlaceHeader(lbHeaderTempRH);
              PlaceDetailPair(   lbOther,   lbOtherTRH);          // guest room
              PlaceDetailPair(   lbKitchen, lbKitchenTRH);
              PlaceDetailPair(   lbLR,      lbLRTRH);
              PlaceRightColLabel(lbLRLightAndLamp);
              PlaceDetailPair(   lbMBR,     lbMBRTRH);
              PlaceDetailPair(   lbOffice,  lbOfficeTRH);
              PlaceDetailPair(   lbShop,    lbShopTRH);
            PlaceHeader(lbHeaderOnOff);
              PlaceDetailPair(   lbAC,         lbACOnOff);
              PlaceDetailPair(   lbFurnace,    lbFurnaceOnOff);
              PlaceDetailPair(   lbShopHeater, lbShopHeaterOnOff);
            PlaceHeader(lbHeaderMisc);
              PlaceDetailPair(   lbKitchen2,   lbKitchenPM25);
              PlaceDetailPair(lbFridgeHerald, lbGCFridge);
              PlaceSimple(     lbPower, PowerSimple);
            LastBottom := NextTop
          end   (* TheLabelAdjuster *);

         self.Height := LastBottom + BottomMargin
       end   (* tfmIndoorSensors.FormShow *);

    procedure tfmIndoorSensors.Initialize;
      (* no data yet *)

      var
        I: integer;
        OB: tObserved;

      begin (* tfmIndoorSensors.Initialize *)
        self.Color := UsualFormColor;
        for I := 1 to ComponentCount do
          if Components[I-1] is tControl then
            tControl(Components[I-1]).Enabled := false;
        LRLampStatusKnown := false;
        LRLightLevel      := -abs(SillyRealValue);
        LeftAmps          := -abs(SillyRealValue);
        RightAmps         := -abs(SillyRealValue);
        for OB := low(tObserved) to high(tObserved) do
          RoomCO2[OB] := SillyIntegerValue
      end   (* tfmIndoorSensors.Initialize *);

    procedure tfmIndoorSensors.LRLightAndLamp;
      (* data in hand, so show it *)

      var
        LampString, TimeString: string;

      begin (* tfmIndoorSensors.LRLightAndLamp *)
        with Global.LatestData do
          TimeString := FormatDateTime('hhnn', LocalStamp);

        with lbLRLightAndLamp do
          begin (* UI *)
            if LRLampNowOn then
              begin (* ON *)
                LampString := 'on';
                Font.Color := clPurple
              end   (* ON *)
            else
              begin (* OFF *)
                LampString := 'off';
                Font.Color := clWindowText
              end   (* OFF *);

            Caption := Format('LR lamp %s, light level %.0f as of %s',
                              [LampString, LRLightLevel, TimeString]);
            Enabled := true
          end   (* UI *)
      end   (* tfmIndoorSensors.LRLightAndLamp *);

    procedure tfmIndoorSensors.NewDataGeneric(const TU: tTableUsed);
      (* will become table-specific *)

      var
        TimeString: string;

      procedure NewCO2;
        (* NewTRH does all the work *)
      
        begin (* NewCO2 *)
          with Global.LatestData do
            RoomCO2[ObservedUID] := IntValueOf(wm2CO2ppm)
        end   (* NewCO2 *);

      procedure NewPower;
        (* must cache left & right amps to present sum *)
      
        begin (* NewPower *)
          with Global.LatestData do
            begin (* Global.LatestData *)
              case ObservedUID of
                 obLeftPower: LeftAmps := RealArg[1];
                obRightPower: RightAmps := RealArg[1]
                end (* case *);
              if (LeftAmps > 0) and (RightAmps > 0) then
                begin (* nondegenerate *)
                  lbPower.Caption :=
                    Format('house drawing  %.1f KW as of %s',
                           [Global.LineVoltage * (LeftAmps + RightAmps)/1000.0,
                            TimeString]);
                  lbPower.Enabled := true
                end   (* nondegenerate *)
            end   (* Global.LatestData *)
        end   (* NewPower *);

      procedure NewSPS30;
        (* select from too much data *)

        var
          PM25: real;

        begin (* NewSPS30 *)
          PM25 := Global.LatestData.RealValueOf(wm2MassPM25);
          lbKitchen2.Enabled := true;
          with lbKitchenPM25 do
            begin (* lbKitchenPM25 *)
              Caption := Format('%.2f µg/m^3 as of %s', [PM25, TimeString]);
              Font.Color := clWindowText;
              Enabled := true
            end   (* lbKitchenPM25 *)
        end   (* NewSPS30 *);

      procedure NewTRH;
        (* new indoor temperature, RH, and (sometimes) CO2ppm.
          dewpoint & pressure not relevant *)

        var
          // GCFridgeSettingx10,
          GCFridgeTemp, MiscTempF: real;
          Report: string;

        procedure InstallAndEnable(ReportLabel, Herald: tLabel);
          (* put the Report in the right place *)

          begin (* InstallAndEnable *)
            ReportLabel.Caption := Report;
            ReportLabel.Enabled := true;
            ReportLabel.Font.Color := clWindowText;
            Herald.Enabled      := true
          end   (* InstallAndEnable *);

        begin (* NewTRH *)
          with Global.LatestData do
            if ObservedUID = obGCFridge then
              begin (* must get setting too *)
                // with dmHC3.qGCFridgeSettingx10 do
                //   begin (* qGCFridgeSettingx10 *)
                //     Close;
                //     Open;
                //     GCFridgeSettingx10 := Fields[0].AsFloat;
                //     Close
                //   end   (* qGCFridgeSettingx10 *);

                GCFridgeTemp := RealValueOf(wm2TempF);
                fmeFrames.eFrameReal(avGCFridge, GCFridgeTemp);
                // Report := Format('setting %.1f temp %.1f°F as of %s',
                //                  [GCFridgeSettingx10/10,
                //                   GCFridgeTemp,
                //                   TimeString]);
                Report := Format('%.1f°F as of %s',
                                 [GCFridgeTemp,
                                  TimeString]);
                InstallAndEnable(lbGCFridge, lbFridgeHerald);
                with lbGCFridge do
                  if (GCFridgeTemp >= FridgeTooHot) or
                     (GCFridgeTemp <= FridgeTooCold) then
                    Font.Color := clPurple
                  else
                    Font.Color := clWindowText
              end   (* must get setting too *)

            else
              begin (* Global.LatestData *)
                MiscTempF := RealValueOf(wm2TempF);
                Report := Format('%.1f °F  %.0f%%',
                                 [MiscTempF, RealValueOf(wm2RH)]);
                if RoomCO2[ObservedUID] <> SillyIntegerValue then
                  Report := Format('%s  CO2 %d ppm', [Report, RoomCO2[ObservedUID]]);
                Report := Format('%s as of %s', [Report, TimeString]);

                case ObservedUID of
                    obKitchen: InstallAndEnable(lbKitchenTRH,   lbKitchen);
                         obLR: InstallAndEnable(lbLRTRH,        lbLR);
                        obMBR: InstallAndEnable(lbMBRTRH,       lbMBR);
                     obOffice: InstallAndEnable(lbOfficeTRH,    lbOffice);
                       obShop: begin (* share with eFrame *)
                                 InstallAndEnable(lbShopTRH,    lbShop);
                                 fmeFrames.eFrameReal(avShopTemp, MiscTempF)
                               end   (* share with eFrame *);
                  obGuestRoom,
                     obTVRoom: begin (* unusual case *)
                                 with lbOther do
                                   Caption := Format('%s: ',
                                                     [GetObservedName(ObservedUID)]);
                                 InstallAndEnable(lbOtherTRH,   lbOther)
                               end   (* unusual case *)
                  end (* case *)
              end   (* Global.LatestData *)
        end   (* NewTRH *);

      begin (* tfmIndoorSensors.NewDataGeneric *)
        with Global.LatestData do
          begin (* Global.LatestData *)
            TimeString := FormatDateTime('hhnn', LocalStamp);
            case TU of
                tuCO2: RoomCO2[ObservedUID] := IntValueOf(wm2CO2ppm);
              tuPower: NewPower;
              tuSPS30: NewSPS30;
               tuTRHP: NewTRH
              end (* case *)
          end   (* Global.LatestData *)
      end   (* tfmIndoorSensors.NewDataGeneric *);

    procedure tfmIndoorSensors.NewDataLRLight(const LightLevel: real);
      (* light level *)

      begin (* tfmIndoorSensors.NewDataLRLight *)
        LRLightLevel := LightLevel;
        if LRLampStatusKnown then
          LRLightAndLamp
      end   (* tfmIndoorSensors.NewDataLRLight *);

    procedure tfmIndoorSensors.NewDataOnOff(const ObservedUID: tObserved;
                                            const AnchorLocal: tDateTime;
                                            const IsNowOn: boolean;
                                            const HoursPassed, HoursOn: real);
      (* complex format *)

      procedure NoteIntent(const IsNowOn: boolean; Label1, Label2: tLabel);
        (* underline if selected at thermostat *)
      
        begin (* NoteIntent *)
          // fmDashBoard.ExtendChat(Format('II_407: observing %s, modifying %s',
          //                               [GetObservedName(ObservedUID),
          //                                Label1.Caption]));
          with Label1.Font do
            if IsNowOn then
              Style := Style + [fsUnderline]
            else
              Style := Style - [fsUnderline];
          Label2.Font.Style := Label1.Font.Style
        end   (* NoteIntent *);

      procedure WorkLabels(TheHerald, TheLabel: tLabel);
        (* dispatched *)

        begin (* WorkLabels *)
          OnOffDisplay(TheHerald, TheLabel, AnchorLocal, IsNowOn, HoursPassed, HoursOn)
        end   (* WorkLabels *);

      begin (* tfmIndoorSensors.NewDataOnOff *)
        case ObservedUID of
                  obAC: WorkLabels(         lbAC,      lbACOnOff);
            obACIntent: NoteIntent(IsNowOn, lbAC,      lbACOnOff);
             obFurnace: WorkLabels(         lbFurnace, lbFurnaceOnOff);
          obHeatIntent: NoteIntent(IsNowOn, lbFurnace, lbFurnaceOnOff);
             obLRRelay: begin (* must merge with light level *)
                          LRLampNowOn := IsNowOn;
                          LRLampStatusKnown := true;
                          if LRLightLevel >= 0 then
                            LRLightAndLamp
                        end   (* must merge with light level *);
          obShopHeater: WorkLabels(lbShopHeater, lbShopHeaterOnOff)
          end (* case *)
      end   (* tfmIndoorSensors.NewDataOnOff *);

end.
