unit table_Pond;
  (* The Pond tables is serviced by disparate sensors.  They
    may be read only rarely, so it's important to start each day.
     The depth gauge does its own damping. *)

  interface
    procedure ProcessCurrentLine_Pond;
    procedure SetPondFilling(const Filling: boolean);

  implementation
    uses
      BoardManager       (* stDepthGauge, stDHT22 *),
      Dashboard          (* fmDashboard, llblParenData *),
      DateUtils          (* DateOf, HourSpan, IncDay, StartOfTheDay *),
      Dialogs            (* ShowMessageFmt *),
      GraphManager2      (* fmGraphManager *),
      HC3DM              (* dmHC3, Global *),
      HC3Utilities       (* INILoadFloat, LastSecondOfTheDay, LastUIDOf,
                              NewDampedValue *),
      LogManager         (* fmLogManager *),
      ObservedAndWM      (* wm2DepthInches, wm2TempF *),
      OutdoorInstruments (* fmOutdoorInstruments *),
      RRF_StringAndTimeUtils (* LocalToUTC *),
      SensorDevice       (* tSensorDevice *),
      SysUtils           (* Format, FormatDateTime *),
      TableManager       (* tuPond *);

    const
      NeedMatchingField      = true;

    type
      tDepthDevice = class(tSensorDevice)
        private
          NewStoredData: boolean;
          TheLocalEdge: tDateTime;
          TheRecentDepthInches: real;
          DefaultDamping: real;
          FillingDamping: real;
          DepthChangeWorthRecording: real;
          procedure StoreNewData(const LocalTime: tDateTime;
                                 const DepthInches: real);
        public
          PondFilling: boolean;
          constructor Create;
          procedure   NewData;
        end (* tDepthDevice *);

      tTempDevice = class(tSensorDevice)
        private
          TheLocalEdge: tDateTime;
          TheRecentTempF: real;
          TempChangeWorthRecording: real;
          procedure StoreNewData(const LocalTime: tDateTime;
                                 const TempF: real);
        public
          constructor Create;
          procedure   NewData;
        end (* tTempDevice *);

    var
      DepthDevice: tDepthDevice;
      MaxNonGraphHours: real;
      TempDevice: tTempDevice;

    procedure ProcessCurrentLine_Pond;
      (* The data line has one value, either wm2DepthInches or wm2TempF
        devices do the work *)
    
      begin (* ProcessCurrentLine_Pond *)
        with Global.LatestData do
          if NArgs >= 1 then
            case WMArg[1] of
                wm2DepthInches: begin (* depth *)
                                  if not assigned(DepthDevice) then
                                    DepthDevice := tDepthDevice.Create;
                                  DepthDevice.NewData
                                end   (* depth *);

                      wm2TempF: begin (* temperature *)
                                  if not assigned(TempDevice) then
                                    TempDevice := tTempDevice.Create;
                                  TempDevice.NewData
                                end   (* temperature *)
              end (* case *)
      end   (* ProcessCurrentLine_Pond *);

    procedure SetPondFilling(const Filling: boolean);
      (* set damping intensity *)
    
      begin (* SetPondFilling *)
        DepthDevice.PondFilling := Filling
      end   (* SetPondFilling *);

    procedure SubMinimalChange(const RecentX, NewX, Minimum: real);
      (* annotate log *)
    
      begin (* SubMinimalChange *)
        fmDashboard.LogBoring(TableInfo[tuPond].TheBoringChar);
        // fmLogManager.ExtendLog(Format('      recent %.4f, new %.4f; %.4f < %.4f ***************',
        //                               [RecentX, NewX, abs(NewX - RecentX), Minimum] ))
      end   (* SubMinimalChange *);

    procedure StorePondData(const LocalTime: tDateTime;
                            const ParamName: string;
                            const Datum: real);
      (* query does the work *)
    
      var
        NewUID: integer;

      begin (* StorePondData *)
        NewUID := LastUIDOf('Pond') + 1;
        with dmHC3.qNewPondData do
          begin (* qNewPondTempF *)
            ParamByName('UID'            ).AsInteger  := NewUID;
            ParamByName('WhenRecorded'   ).AsDateTime := LocalTime;
            ParamByName('WhenRecordedUTC').AsDateTime := LocalToUTC(LocalTime);

            ParamByName('TempF'          ).Clear;
            ParamByName('DepthInches'    ).Clear;
            ParamByName(ParamName).AsFloat := Datum;
            ExecSQL
          end   (* qNewPondTempF *)
      end   (* StorePondData *);

{ tTempDevice }

    constructor tTempDevice.Create;
      (* constructor *)

      var
        LocalEdge: tDateTime;
        RecentTempF: real;

      begin (* tTempDevice.Create *)
        inherited Create(tuPond, obPond, Global.LatestData.UTCStamp, stDHT22);
        MaxNonGraphHours         := INILoadFloat('Pond', 'MaxNonGraphHours');
        TempChangeWorthRecording := INILoadFloat(        'MinTempDifference');
        with dmHC3.qGetRecentPondTempF do
          begin (* qGetRecentPondTempF *)
            Open;
            LocalEdge   := FieldByName('WhenRecorded').AsDateTime;
            RecentTempF := FieldByName('TempF'       ).AsFloat;
            Close;
          end   (* qGetRecentPondTempF *);

        TheLocalEdge   := LocalEdge;
        TheRecentTempF := RecentTempF;
        with fmDashboard do
          ExtendChat(FormatDateTime('"pd_155 created device for pond temperature, edge "yyyy-mm-dd',
                                    TheLocalEdge),
                     llblCreated)
      end   (* tTempDevice.Create *);

    procedure tTempDevice.NewData;
      (* start day(s) if necessary, just store it *)

      var
        NewTempF: real;

      begin (* tTempDevice.NewData *)
        with Global.LatestData do
          begin (* Global.LatestData *)
            // if DateOf(TheLocalEdge) < DateOf(LocalStamp) then
            //   ShowMessageFmt('pd_170 LocalEdge %s LocalStamp %s',
            //                  [FormatDateTime('yyyy-mm-dd', TheLocalEdge),
            //                   FormatDateTime('yyyy-mm-dd', LocalStamp)]);
            while DateOf(TheLocalEdge) < DateOf(LocalStamp) do
              begin (* force start of today *)
                // with fmDashboard do
                //   ExtendChat(FormatDateTime(
                //     '"pd_177 catching pond temp up from "yyyy-mm-dd', TheLocalEdge));
                StoreNewData(LastSecondOfTheDay(TheLocalEdge), TheRecentTempF);
                TheLocalEdge := StartOfTheDay(IncDay(TheLocalEdge));
                StoreNewData(TheLocalEdge, TheRecentTempF)
              end   (* force start of today *);

            NewTempF := RealArg[1];
            if (HourSpan(LocalStamp, TheLocalEdge) > MaxNonGraphHours) or
               (abs(NewTempF - TheRecentTempF) >= TempChangeWorthRecording) then
              StoreNewData(LocalStamp, NewTempF)
            else
              SubMinimalChange(TheRecentTempF, NewTempF, TempChangeWorthRecording)
          end   (* Global.LatestData *);

        fmOutdoorInstruments.NewDataGeneric(tuPond);
        if Global.Status <> gsReceivingDump then
          fmGraphManager.UpdateGraphs(TheSensorUID)
      end   (* tTempDevice.NewData *);

    procedure tTempDevice.StoreNewData(const LocalTime: tDateTime;
                                       const TempF: real);
      (* query does the work *)

      begin (* tTempDevice.StoreNewData *)
        StorePondData(LocalTime, 'TempF', TempF);
        TheLocalEdge   := LocalTime;
        TheRecentTempF := TempF
      end   (* tTempDevice.StoreNewData *);

  { tDepthDevice }

    constructor tDepthDevice.Create;
      (* constructor *)

      var
        LocalEdge: tDateTime;
        RecentDepthInches: real;

     begin (* tDepthDevice.Create *)
        inherited Create(tuPond, obPond, Global.LatestData.UTCStamp, stDepthGauge);
        MaxNonGraphHours          := INILoadFloat('Pond', 'MaxNonGraphHours');
        DepthChangeWorthRecording := INILoadFloat(        'MinDepthDifference');
        DefaultDamping            := INILoadFloat(        'DepthDamping');
        FillingDamping            := INILoadFloat(        'DepthFillingDamping');
        PondFilling               := false;

        with dmHC3.qGetRecentPondDepth do
          begin (* qGetRecentPondDepth *)
            Open;
            LocalEdge         := FieldByName('WhenRecorded'   ).AsDateTime;
            RecentDepthInches := FieldByName('DepthInches'    ).AsFloat;
            Close;
          end   (* qGetRecentPondDepth *);

        with fmDashboard do
          ExtendChat(Format('pd_232 created device for pond depth starting %.2f"',
                            [RecentDepthInches]),
                     llblCreated);
        TheLocalEdge         := LocalEdge;
        TheRecentDepthInches := RecentDepthInches
      end   (* tDepthDevice.Create *);

    procedure tDepthDevice.NewData;
      (* start new day if necessary *)

      const
        PlausibleDropInches = 2.0;

      var
        Damping, NewDepthInches: real;

      begin (* tDepthDevice.NewData *)
        NewStoredData := false;
        with Global.LatestData do
          begin (* Global.LatestData *)
            while DateOf(TheLocalEdge) < DateOf(LocalStamp) do
              begin (* force start of today *)
                // with fmDashboard do
                //   ExtendChat(FormatDateTime(
                //     '"pd_256 catching pond depth up from "yyyy-mm-dd', TheLocalEdge));
                StoreNewData(LastSecondOfTheDay(TheLocalEdge), TheRecentDepthInches);
                TheLocalEdge := StartOfTheDay(IncDay(TheLocalEdge));
                StoreNewData(TheLocalEdge, TheRecentDepthInches)
              end   (* force start of today *);
            If PondFilling then
              Damping := FillingDamping
            else
              Damping := DefaultDamping;
            NewDepthInches :=   Damping      * TheRecentDepthInches +
                             (1.0 - Damping) * RealArg[1];

            if (NewDepthInches > TheRecentDepthInches - PlausibleDropInches)
               and ((HourSpan(LocalStamp, TheLocalEdge) > MaxNonGraphHours) or
                    (abs(NewDepthInches - TheRecentDepthInches) >= DepthChangeWorthRecording)
                   ) then
              StoreNewData(LocalStamp, NewDepthInches)
            else
              SubMinimalChange(TheRecentDepthInches, NewDepthInches, DepthChangeWorthRecording)
          end   (* Global.LatestData *);

        fmOutdoorInstruments.NewDataGeneric(tuPond);
        if Global.Status <> gsReceivingDump then
          if NewStoredData then
            fmGraphManager.UpdateGraphs(TheSensorUID)
      end   (* tDepthDevice.NewData *);

    procedure tDepthDevice.StoreNewData(const LocalTime: tDateTime;
                                        const DepthInches: real);
      (* query does the work *)
    
      begin (* tDepthDevice.StoreNewData *)
        StorePondData(LocalTime, 'DepthInches', DepthInches);
        TheLocalEdge         := LocalTime;
        TheRecentDepthInches := DepthInches;
        NewStoredData := true
      end   (* tDepthDevice.StoreNewData *);

  initialization
    DepthDevice := nil;
    TempDevice  := nil;

end.
