unit RelayManager;
  (* receives wmTargetF, wmRelayAck *)

  interface
    uses
      AdsData          (* tAdsStringField *),
      AdsFunc          (* used by AdsTable *),
      AdsTable         (* tAdsQuery *),
      AdvGrid          (* glCustom *),
      AdvObj           (* TMS *),
      AdvUtil          (* TMS *),
      BaseGrid         (* TMS *),
      Buttons          (* tBitBtn *),
      Classes          (**),
      ComCtrls         (* tDateTimePicker *),
      Controls         (* tTime, tWinControl *),
      DB               (* tDataSet, tDataSource, tIntegerField, tStringField;
                            also used by AdsTable *),
      DBAdvGrid        (* tDBAdvGrid *),
      ExtCtrls         (* tRadioGroup *),
      Forms            (* tForm *),
      Graphics         (* TMS *),
      Grids            (* TMS *),
      LabeledSpinEdits (* tLabeledSpinFloat, tLabeledSpinInteger*),
      MyConsts         (* nullstring, UsualFormColor *),
      ObservedAndWM    (* tWindowsToBase, wtbRelayAction, wtbRelayFallback,
                            wtbRelayLight, wtbRelayMode,
                            wtbRelayOff, wtbRelayOn, wtbRelaySchedule,
                            wtbRelayWhichTemp *),
      RRFSpinBase      (* used by LabeledSpinEdits *),
      StdCtrls         (* tButton, tGroupBox *),
      Windows          (* tRect *);

    type
      tfmRelayManager = class(tForm)
          qAdvanceEdge: tAdsQuery;
          qNewPair:     tAdsQuery;
          qRelays:      tAdsQuery;
            qRelaysBoardName:       tAdsStringField;
            qRelaysBoardTag:        tAdsStringField;
            qRelaysNote:            tAdsStringField;
            qRelaysObservedName:    tAdsStringField;
            qRelaysRelayKind:       tAdsStringField;
            qRelaysSlotName:        tAdsStringField;
            qRelaysStationLocation: tAdsStringField;
            qRelaysObservedUID: tIntegerField;
            qRelaysXBeeMY:      tIntegerField;
            qRelaysXBoardName: tStringField;
            qRelaysXXBeeMy:    tStringField;
            dataRelays: tDataSource;
              dbadvgridRelays: tDBAdvGrid;
              qIdentifyTemps: tAdsQuery;
            btnManualOff: tButton;
            btnManualOn:  tButton;
          grpSchedule: tGroupBox;
            lbspintStartTime: tLabeledSpinInteger;
            lbspintStopTime:  tLabeledSpinInteger;
          lbForcedState: tLabel;
          lbRelayInfo: tLabel;
          pgctlRelaySpecific: tPageControl;
            tabHeaterControl: tTabSheet;
              btnSetHeaterControl: tButton;
              lbedHeaterTempOfInterest: tLabeledEdit;
              lbspflTempHysteresis: tLabeledSpinFloat;
              lbspintFallbackTempTarget:  tLabeledSpinInteger;
              lbspintInBracketTempTarget: tLabeledSpinInteger;
            tabNightLight: tTabSheet;
              btnSetNightlight: tButton;
              lbspintNightlightOffIfLighterThan: tLabeledSpinInteger;
              lbspintNighlightOnIfDarkerThan:    tLabeledSpinInteger;
            tabWaterfall: tTabSheet;
              btnSetWaterfall: tButton;
              grpWaterfallLight: tGroupBox;
                lbspintWaterfallOffIfDarkerThan: tLabeledSpinInteger;
                lbspintWaterfallOnIfLighterThan: tLabeledSpinInteger;
              grpWaterfallTemp: tGroupBox;
                lbspintWaterfallOnIfWarmerThan:  tLabeledSpinInteger;
                lbspintWaterfallOffIfColderThan: tLabeledSpinInteger;
              lbedWaterfallTempOfInterest: tLabeledEdit;
              lbWaterfallOff:            tLabel;
              lbWaterfallOn:             tLabel;

          procedure qRelaysAfterScroll(DataSet: tDataSet);
          procedure qRelaysCalcFields (DataSet: tDataSet);

          procedure btnManualOffClick       (Sender: tObject);
          procedure btnManualOnClick        (Sender: tObject);
          procedure btnSetHeaterControlClick(Sender: tObject);
          procedure btnSetNightlightClick   (Sender: tObject);
          procedure btnSetWaterfallClick    (Sender: tObject);
          procedure FormDblClick            (Sender: tObject);
          procedure FormShow                (Sender: tObject);

        private
          PinIndex: integer;
          BoardName: string;
          procedure OpenHeaterControl;
          procedure OpenNightlight;
          procedure OpenTimeBracket;
          procedure OpenWaterfall;
          procedure SendToXBee(const Args: array of integer;
                               ForChat: string = nullstring);
          procedure SendNoArgs (const WTB: tWindowsToBase;
                                    ForChat: string = nullstring);
          procedure SendOneArg (const WTB: tWindowsToBase; const Arg: integer;
                                    ForChat: string = nullstring);
          procedure SendTwoArgs(const WTB: tWindowsToBase; const Arg1, Arg2: integer;
                                    ForChat: string = nullstring);
          procedure SetSchedule;
          procedure StateNotForced;
        end (* tfmRelayManager *);

    var
      fmRelayManager: tfmRelayManager;

  implementation
    uses
      Dashboard      (* ConnectionMgr *),
      DateUtils      (* HourOf, MinuteOf, TimeOf, Today *),
      DBAdvGridUtils (* FieldNameToColumn, MarlettCheck, MarlettCheckField, NormalBrush,     // *********************
                              NormalPen, ReadOnlyBrush, ReadOnlyPen,
                              SetUpDBAdvGridReadOnly, SortBrush, SortOn, StretchField *),
      Dialogs        (* ShowMessage *),
      HC3DM          (* Global *),
      HC4Main        (* fmHC4Main *),
      HC3Utilities   (* INILoadFloat, INILoadInteger, INISaveFloat,
                          INISaveInteger, LastSecondOfTheDay, MilTimeToMSM,
                          MSMToMilTime *),
      MyUtils        (* EmphasizeSelectedDBGridLine *),
      SysUtils       (* Format, IntToStr, StrToInt *);
      {$R *.dfm}

    const
      VSpace = 15;

    type
      tRelayMode = (rmForced, rmHeaterControl, rmNightlight, rmWaterfall);

    var
      XBeeMY: integer;
      Section: string;          // sticky for INI

{************ infrastructure **************************************************}
    procedure tfmRelayManager.btnManualOffClick(Sender: tObject);
      (* force off *)
    
      begin (* tfmRelayManager.btnManualOffClick *)
        INISaveString(Section, 'ForcedState', 'off');
        lbForcedState.Caption := 'forced off';
        lbForcedState.Visible := true;
        SendTwoArgs(wtbRelayMode, ord(rmForced), PinIndex);
        SendNoArgs(wtbRelayOff, tButton(Sender).Caption);
        SendNoArgs(wtbRelayAction)
      end   (* tfmRelayManager.btnManualOffClick *);

    procedure tfmRelayManager.btnManualOnClick(Sender: tObject);
      (* force on *)

      begin (* tfmRelayManager.btnManualOnClick *)
        INISaveString(Section, 'ForcedState', 'on');
        lbForcedState.Caption := 'forced on';
        lbForcedState.Visible := true;
        SendTwoArgs(wtbRelayMode, ord(rmForced), PinIndex);
        SendNoArgs(wtbRelayOn, tButton(Sender).Caption);
        SendNoArgs(wtbRelayAction)
      end   (* tfmRelayManager.btnManualOnClick *);

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

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

    procedure tfmRelayManager.FormShow(Sender: tObject);
      (* initialize *)

      begin (* tfmRelayManager.FormShow *)
        self.Color       := UsualFormColor;
        qRelays.Open;
        SetUpDBAdvGridReadOnly(dbadvgridRelays);
        btnManualOff.Tag := ord(wtbRelayOff);
        btnManualOn.Tag  := ord(wtbRelayOn)
      end   (* tfmRelayManager.FormShow *);

    procedure tfmRelayManager.OpenTimeBracket;
      (* load UI from INI *)
    
      begin (* tfmRelayManager.OpenTimeBracket *)
        lbspintStartTime.Value := MSMToMilTime(INILoadInteger('StartTime'));
        lbspintStopTime.Value  := MSMToMilTime(INILoadInteger('StopTime' ))
      end   (* tfmRelayManager.OpenTimeBracket *);

    procedure tfmRelayManager.qRelaysAfterScroll(DataSet: tDataSet);
      (* just a shortcut *)

      var
        //                      Relay_x
        ForcedState, RelayKind, SlotName: string;

      begin (* tfmRelayManager.qRelaysAfterScroll *)
        tabHeaterControl.TabVisible  := false;
        tabNightLight.TabVisible     := false;
        tabWaterfall.TabVisible      := false;

        grpSchedule.Visible := true;
        with DataSet do
          begin (* DataSet *)
            BoardName := FieldByName('BoardName').AsString;
            RelayKind := FieldByName('RelayKind').AsString;
            SlotName  := FieldByName('SlotName').AsString;
            XBeeMY    := FieldByName('XBeeMY'   ).AsInteger
          end   (* DataSet *);
        PinIndex := StrToInt(copy(SlotName, 7, maxint)) - 1;
        lbRelayInfo.Caption := 'no relay connected';
        if RelayKind <> nullstring then
          case RelayKind[1] of
            'H': OpenHeaterControl;
            'N': OpenNightlight;
            'W': OpenWaterfall;
            end (* case *);

        ForcedState := INILoadString(Section, 'ForcedState', nullstring);
        with lbForcedState do
          begin (* lbForcedState *)
            Visible := true;
            if      ForcedState = 'on' then  Caption := 'forced on'
            else if ForcedState = 'off' then Caption := 'forced off'
            else                             Visible := false
          end   (* lbForcedState *)
      end   (* tfmRelayManager.qRelaysAfterScroll *);

    procedure tfmRelayManager.qRelaysCalcFields(DataSet: tDataSet);
      (* prettyprinting *)

      var
        XBeeMY: integer;
        BoardName, BoardTag: string;

      begin (* tfmRelayManager.qRelaysCalcFields *)
        with qRelays do
          begin (* qRelays *)
            BoardName   := FieldByName('BoardName'  ).AsString;
            BoardTag    := FieldByName('BoardTag'   ).AsString;
            XBeeMY      := FieldByName('XBeeMY'     ).AsInteger;

            with FieldByName('XBoardName') do
              AsString := Format('%s [%s]', [BoardName, BoardTag]);
            with FieldByName('XXBeeMY') do
              AsString := Format('%d ($%x)', [XBeeMY, XBeeMY])
          end   (* qRelays *)
      end   (* tfmRelayManager.qRelaysCalcFields *);

    procedure tfmRelayManager.SetSchedule;
      (* used by nightlight and heater *)

      begin (* tfmRelayManager.SetSchedule *)
        SendTwoArgs(wtbRelaySchedule,
                    MilTimeToMSM(lbspintStartTime.Value), MilTimeToMSM(lbspintStopTime.Value),
                    Format('active daily, %.4d to %.4d',
                           [lbspintStartTime.Value, lbspintStopTime.Value]))
      end   (* tfmRelayManager.SetSchedule *);

    procedure tfmRelayManager.StateNotForced;
      (* cliche *)

      begin (* tfmRelayManager.StateNotForced *)
        INISaveString(Section, 'ForcedState', 'no');
        lbForcedState.Visible := false
      end   (* tfmRelayManager.StateNotForced *);

{************ end infrastructure **********************************************}
{************ communication ***************************************************}
    procedure tfmRelayManager.SendNoArgs(const WTB: tWindowsToBase;
                                         ForChat: string);
      (* front end for front end *)

      begin (* tfmRelayManager.SendNoArgs *)
        // ShowMessageFmt('RM.SendNoArgs(%d, "%s")', [ord(WTB), ForChat]);
        SendToXBee([XBeeMY, ord(WTB)], ForChat)
      end   (* tfmRelayManager.SendNoArgs *);

    procedure tfmRelayManager.SendOneArg(const WTB: tWindowsToBase;
                                         const Arg: integer;
                                         ForChat: string);
      (* front end for front end *)

      begin (* tfmRelayManager.SendOneArg *)
        SendToXBee([XBeeMY, ord(WTB), Arg], ForChat)
      end   (* tfmRelayManager.SendOneArg *);

    procedure tfmRelayManager.SendToXBee(const Args: array of integer;
                                         ForChat: string = nullstring);
      (* common code *)

      // var
        // I: integer;
        // strArgs: string;

      begin (* tfmRelayManager.SendToXBee *)
        // strArgs := nullstring;
        // if length(Args) > 0 then
        //   for I := 0 to high(Args) do
        //     if I = 0 then
        //       strArgs := IntToStr(Args[0])
        //     else
        //       strArgs := Format('%s, %d', [strArgs, Args[I]]);
        // ShowMessageFmt('RM.SendToXBee([%s] "%s")', [strArgs, ForChat]);

        if Global.Status <> gsUnconnected then
          with fmDashboard.SerialManager do
            IntMessageToBaseStation(wtbSendToXBee, Args,
                                    Format('relay at XBee #%d: %s',
                                           [XBeeMY, ForChat]))
      end   (* tfmRelayManager.SendToXBee *);

    procedure tfmRelayManager.SendTwoArgs(const WTB: tWindowsToBase;
                                          const Arg1, Arg2: integer;
                                          ForChat: string);
      (* front end for front end *)

      begin (* tfmRelayManager.SendTwoArgs *)
        SendToXBee([XBeeMY, ord(WTB), Arg1, Arg2], ForChat)
      end   (* tfmRelayManager.SendTwoArgs *);

{************ end communicvation **********************************************}
{************ heater control **************************************************}
    procedure tfmRelayManager.btnSetHeaterControlClick(Sender: tObject);
      (* TempOfInterest here a kludge; should look up slot from Observed
           possibilities are
             0 DHT22_1
             1 DHT22_2
             2 BMP085
             3 SCD40
             4 SCD30
             5 broadcast
           *)

      var
        FallbackTarget, InBracketTarget, WhichTemp: integer;
        TempHysteresis: real;

      begin (* tfmRelayManager.btnSetHeaterControlClick *)
        FallbackTarget  := lbspintFallbackTempTarget.Value;
        InBracketTarget := lbspintInBracketTempTarget.Value;
        TempHysteresis  := lbspflTempHysteresis.Value;
        if (Section = 'shop heater') or (Section = 'prototype heater') then
          WhichTemp := 1
        else
          WhichTemp := 5;

        INISaveFloat(Section, 'TempHysteresis',      TempHysteresis);
        INISaveInteger(       'InBracketTempTarget', InBracketTarget);
        INISaveInteger(       'FallbackTempTarget',  FallbackTarget);
        INISaveInteger(       'StartTime',           MilTimeToMSM(lbspintStartTime.Value));
        INISaveInteger(       'StopTime',            MilTimeToMSM(lbspintStopTime.Value));
        INISaveString(        'TempOfInterest',      lbedHeaterTempOfInterest.Text);
        StateNotForced;

        SendTwoArgs(wtbRelayMode, ord(rmHeaterControl), PinIndex);
        SetSchedule;
        //  Target and fallback
        SendTwoArgs(wtbRelayTemps, FallbackTarget, InBracketTarget,
                    Format('target %d°F with hysteresis %.1f, fallback %d°',
                           [InBracketTarget, TempHysteresis, FallbackTarget]));
        //  hysteresis
        SendOneArg(wtbRelayHysteresis, trunc(10 * TempHysteresis));
        SendOneArg(wtbRelayWhichTemp, WhichTemp);
        SendNoArgs(wtbRelayAction)
      end   (* tfmRelayManager.btnSetHeaterControlClick *);

    procedure tfmRelayManager.OpenHeaterControl;                      
      (* load UI from INI *)
    
      begin (* tfmRelayManager.OpenHeaterControl *)
        lbRelayInfo.Caption := 'relay used for heater control';
        pgctlRelaySpecific.ActivePage    := tabHeaterControl;
        tabHeaterControl.TabVisible      := true;
        Section                          := BoardName + ' heater';
        lbspintInBracketTempTarget.Value := INILoadInteger(Section, 'InBracketTempTarget');
        lbspintFallbackTempTarget.Value  := INILoadInteger(         'FallbackTempTarget');
        lbspflTempHysteresis.Value       := INILoadFloat  (         'TempHysteresis');
        lbedHeaterTempOfInterest.Text    := INILoadString (         'TempOfInterest');
        OpenTimeBracket
      end   (* tfmRelayManager.OpenHeaterControl *);

{************ end heater control **********************************************}
{************ nightlight ******************************************************}
    procedure tfmRelayManager.btnSetNightlightClick;
        (* hysteresis precomputed *)

      var
        Darker, Lighter: integer;

      begin (* tfmRelayManager.btnSetNightlightClick *);
        Darker  := lbspintNighlightOnIfDarkerThan.Value;
        Lighter := lbspintNightlightOffIfLighterThan.Value;

        INISaveInteger(Section, 'OffIfLighterThan', Lighter);
        INISaveInteger(         'OnIfDarkerThan',   Darker);
        INISaveInteger(         'StartTime',        MilTimeToMSM(lbspintStartTime.Value));
        INISaveInteger(         'StopTime',         MilTimeToMSM(lbspintStopTime.Value));
        StateNotForced;

        SendTwoArgs(wtbRelayMode, ord(rmNightlight), PinIndex);
        SetSchedule;
        SendTwoArgs(wtbRelayLight, Darker, Lighter,
                      Format('nightlight light %d-%d', [Darker, Lighter]));
        SendNoArgs(wtbRelayAction)
      end   (* tfmRelayManager.btnSetNightlightClick *);

    procedure tfmRelayManager.OpenNightlight;
       (* fill UI from INI *)

       begin (* tfmRelayManager.OpenNightlight *)
          lbRelayInfo.Caption                     := 'relay used for nightlight';
          pgctlRelaySpecific.ActivePage           := tabNightLight;
          tabNightLight.TabVisible                := true;
          Section                                 := BoardName;
          lbspintNightlightOffIfLighterThan.Value := INILoadInteger(Section, 'OffIfLighterThan');
          lbspintNighlightOnIfDarkerThan.Value    := INILoadInteger(         'OnIfDarkerThan');
          OpenTimeBracket
       end   (* tfmRelayManager.OpenNightlight *);

{************ end nightlight **************************************************}
{************ waterfall *******************************************************}
    procedure tfmRelayManager.btnSetWaterfallClick(Sender: tObject);
      (* hysteresis precomputed *)

      var
        Colder, Darker, Hotter, Lighter, WhichTemp: integer;
        TempOfInterest: string;

      begin (* tfmRelayManager.btnSetWaterfallClick *)
        Colder         := lbspintWaterfallOffIfColderThan.Value;
        Darker         := lbspintWaterfallOffIfDarkerThan.Value;
        Hotter         := lbspintWaterfallOnIfWarmerThan.Value;
        Lighter        := lbspintWaterfallOnIfLighterThan.Value;
        TempOfInterest := lbedWaterfallTempOfInterest.Text;

        if      TempOfInterest = 'outside' then WhichTemp := 6
        else if TempOfInterest = 'pond'    then WhichTemp := 5
        else                                    WhichTemp := 6;

        INISaveString (Section,   'TempOfInterest',  TempOfInterest);
        INISaveInteger(           'OffIfDarkerThan', Darker);
        INISaveInteger(           'OnIfLighterThan', Lighter);
        INISaveInteger(           'OffIfColderThan', Colder);
        INISaveInteger(           'OnIfWarmerThan',  Hotter);
        StateNotForced;

        SendTwoArgs(wtbRelayMode, ord(rmWaterfall), PinIndex);
        SendTwoArgs(wtbRelayTemps, Colder, Hotter,
                    Format('waterfall temps %d-%d', [Colder, Hotter]));
        SendTwoArgs(wtbRelayLight, Darker, Lighter,
                     Format('waterfall light %d-%d', [Darker, Lighter]));
        SendOneArg(wtbRelayWhichTemp, WhichTemp,
                     Format('waterfall using %s temp', [TempOfInterest]));
        SendNoArgs(wtbRelayAction)
      end   (* tfmRelayManager.btnSetWaterfallClick *);

    procedure tfmRelayManager.OpenWaterfall;
      (* load UI from INI *)
    
      begin (* tfmRelayManager.OpenWaterfall *)
        lbRelayInfo.Caption                   := 'relay used for waterfall';
        pgctlRelaySpecific.ActivePage         := tabWaterfall;
        tabWaterfall.TabVisible               := true;
        Section                               := 'waterfall';
        grpSchedule.Visible                   := false;
        lbedWaterfallTempOfInterest.Text      := INILoadString(Section,    'TempOfInterest', nullstring);
        lbspintWaterfallOffIfDarkerThan.Value := INILoadInteger(           'OffIfDarkerThan');
        lbspintWaterfallOnIfLighterThan.Value := INILoadInteger(           'OnIfLighterThan');
        lbspintWaterfallOffIfColderThan.Value := INILoadInteger(           'OffIfColderThan');
        lbspintWaterfallOnIfWarmerThan.Value  := INILoadInteger(           'OnIfWarmerThan' );
      end   (* tfmRelayManager.OpenWaterfall *);

{************ end waterfall ***************************************************}

end.
