
    5jv$                     H   d Z ddlZddlZddlZddlZddlmZ ddlmZ  eej                  j                  d ee
      j                         j                  dz              Z ej                         Zd Zd Zd	 Zd
 Zd ZddZd Zd ZddZd Zd Zd Zd ZddZd Zy)u  
thingpark_store.py — SQLite store for LoRaWAN uplinks from ThingPark (DVI-1136).

Durable store for telemetry pushed by the Actility ThingPark Enterprise
Application Server (Generic HTTPS connector → POST /thingpark/ingest). Live
LoRaWAN telemetry is push-only, so Togen receives uplinks here; the Auditor
table, and later the Equipment/Oversight layers, read them back — degrading to
the last stored snapshot when ThingPark is unreachable (same contract as
scada_store.py).

Flask-independent (mirrors scada_store.py / scada_report.py) so app.py, any
future scheduler, and tests can all import it. The DB lives alongside the
app's other state files (thingpark.db next to thingpark_config.json).

Schema
------
uplinks(dev_eui, ts, fcnt_up, ...) — one row per received uplink. Idempotent
    ingest via UNIQUE(dev_eui, fcnt_up, ts): ThingPark retries failed POSTs and
    a device's frame counter is unique within a session, so the same uplink is
    stored once. Decoded payload (ThingPark X driver JSON) is kept verbatim in
    ``decoded`` for flexible surfacing before we pin per-model columns.
meta(key, value) — ingest health bookkeeping for UI surfacing (why data
    stopped, mirroring the SCADA collector-health pattern).
    N)datetime)PathTHINGPARK_DB_FILEzthingpark.dbc                      t        j                  t        t              d      } | j	                  d       | j	                  d       | S )N   )timeoutzPRAGMA journal_mode=WALzPRAGMA busy_timeout=10000)sqlite3connectstrr   executeconns    &/var/www/html/togen/thingpark_store.py_connectr   )   s7    ??3012>DLL*+LL,-K    c                      t         5  t               5 } | j                  d       | j                  d       | j                  d       | j                  d       ddd       ddd       y# 1 sw Y   xY w# 1 sw Y   yxY w)zECreate tables/indexes if missing. Idempotent; call at import/startup.a  CREATE TABLE IF NOT EXISTS uplinks (
                   id INTEGER PRIMARY KEY AUTOINCREMENT,
                   dev_eui TEXT NOT NULL,
                   ts TEXT NOT NULL,
                   fcnt_up INTEGER,
                   fport INTEGER,
                   payload_hex TEXT,
                   rssi REAL,
                   snr REAL,
                   battery REAL,
                   lat REAL,
                   lng REAL,
                   decoded TEXT,
                   source TEXT NOT NULL DEFAULT 'thingpark',
                   created_at TEXT NOT NULL DEFAULT (datetime('now')),
                   UNIQUE(dev_eui, fcnt_up, ts)
               )zHCREATE INDEX IF NOT EXISTS idx_tp_uplinks_dev_ts ON uplinks(dev_eui, ts)z;CREATE INDEX IF NOT EXISTS idx_tp_uplinks_ts ON uplinks(ts)zBCREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)N)_write_lockr   r   r   s    r   init_thingpark_dbr   0   s    	 Rhj RD	$ 	'	( 	I	KP	R1R R R R R Rs#   A4AA(A4(A1	-A44A=c                     | xs dj                         }|s|S 	 t        j                  |j                  dd            }|j	                  d      S # t
        $ r |cY S w xY w)a+  Normalize an uplink timestamp to sortable "YYYY-MM-DD HH:MM:SS" text.

    ThingPark stamps uplinks with ISO-8601 (``Time``/``time``, often with a
    timezone offset). Unparseable input is returned trimmed (stored verbatim,
    sorts best-effort) so we never drop an uplink over a stamp quirk.
     Zz+00:00%Y-%m-%d %H:%M:%S)stripr   fromisoformatreplacestrftime
ValueError)rawsdts      r   normalize_tsr!   N   sb     
A##AIIc8$<={{.// s   5A AAc                 r    	 t        t        |       j                               S # t        t        f$ r Y y w xY wN)floatr   r   	TypeErrorr   vs    r   	_as_floatr(   _   s3    SV\\^$$z" s   !$ 66c                     	 t        t        t        |       j                                     S # t        t
        f$ r Y y w xY wr#   )intr$   r   r   r%   r   r&   s    r   _as_intr+   f   s8    5Q())z" s   *- ??c                 |   g }| xs g D ]  }t        |t              st        |j                  d      xs d      j	                         }t        t        |j                  d      xs d            }|r|sm|j                  d      }t        |t        t        f      rt        j                  |d      }n|t        |      }|j                  |dd |t        |j                  d	            t        |j                  d
            |j                  d      +t        |j                  d            j	                         dd ndt        |j                  d            t        |j                  d            t        |j                  d            t        |j                  d            t        |j                  d            ||f        |syt        5  t               5 }|j                  }|j                  d|       |j                  |z
  cddd       cddd       S # 1 sw Y   nxY wddd       y# 1 sw Y   yxY w)a+  Insert uplink dicts idempotently; return the number of NEW rows stored.

    Each row needs at least ``dev_eui`` and ``ts``. Numeric fields are coerced
    (None when absent/unparseable); ``decoded`` is JSON-serialized if given a
    dict/list. Duplicates on (dev_eui, fcnt_up, ts) are ignored.
    dev_euir   tsdecoded),:)
separatorsN    fcnt_upfportpayload_hexi   rssisnrbatterylatlngr   zINSERT OR IGNORE INTO uplinks (dev_eui, ts, fcnt_up, fport, payload_hex, rssi, snr, battery,  lat, lng, decoded, source) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?))
isinstancedictr   getr   r!   listjsondumpsappendr+   r(   r   r   total_changesexecutemany)	rowssourcepreparedrr-   r.   r/   r   befores	            r   insert_uplinksrJ   m   s    HZR 7!T"aeeI&,"-335#aeeDk/R01b%%	"gd|,jjZ@G 'lGCRL"gaeeI&67w9Pm$0 }%&,,.t46:aeeFm$ie&=aeeI&'155<)@aeeEl#Wf6 	77& 	 +hj +D##; =E		F
 !!F*+ + + + + + +s$   H2-H
	H2H&	"H22H;c                     | \  }}}}}}}}}	}
}d }|r	 t        j                  |      }|||||||||	|
|dS # t        t        f$ r |}Y !w xY w)N)r-   	last_seenr4   r5   r6   r7   r8   r9   r:   r;   r/   )r@   loadsr   r%   )rowr-   r.   r4   r5   r6   r7   r8   r9   r:   r;   r/   decs                r   _row_to_devicerP      s|    Wb'5+tS'#w
C	**W%C {D3ss  I& 	C	s   7 A
Ac                      t               5 } | j                  d      j                         }ddd       D cg c]  }t        |       }}t	        d |D        d      }||dS # 1 sw Y   :xY wc c}w )a  Most recent stored uplink per device: {"time": <max ts>, "devices":
    [{dev_eui, last_seen, rssi, snr, battery, lat, lng, decoded, ...}]}.

    Per-device ts lets the Auditor table age each tag independently (a dead
    tag must show stale even while its neighbors keep reporting).a~  SELECT u.dev_eui, u.ts, u.fcnt_up, u.fport, u.payload_hex,
                      u.rssi, u.snr, u.battery, u.lat, u.lng, u.decoded
                 FROM uplinks u
                WHERE u.id = (SELECT u2.id FROM uplinks u2
                               WHERE u2.dev_eui = u.dev_eui
                            ORDER BY u2.ts DESC, u2.id DESC LIMIT 1)
             ORDER BY u.dev_euiNc              3   &   K   | ]	  }|d      yw)rL   N ).0ds     r   	<genexpr>z"latest_snapshot.<locals>.<genexpr>   s     2Q!K.2s   )default)timedevices)r   r   fetchallrP   max)r   rE   rH   rY   latests        r   latest_snapshotr]      sz     
 /t||#$ %-HJ 	/ +//Q~a /G/2'2DAFw/// / 0s    A$A0$A-c                     t               5 }|j                  d| |f      j                         }ddd       D cg c]  }t        |       c}S # 1 sw Y   "xY wc c}w )zBRecent uplinks for one device, newest first (detail/history view).zSELECT dev_eui, ts, fcnt_up, fport, payload_hex, rssi, snr,
                      battery, lat, lng, decoded
                 FROM uplinks WHERE dev_eui = ?
             ORDER BY ts DESC, id DESC LIMIT ?N)r   r   rZ   rP   )r-   limitr   rE   rH   s        r   device_uplinksr`      sj    	 Pt||2 5<U3C	E FNXZ	 	P (,,!N1,,P P -s   #AAAc                      t               5 } | j                  d      j                         d   cd d d        S # 1 sw Y   y xY w)NzSELECT COUNT(*) FROM uplinksr   r   r   fetchoner   s    r   uplink_countrd      s=    	 Jt||:;DDFqIJ J J	   "7A c                      t               5 } | j                  d      j                         d   cd d d        S # 1 sw Y   y xY w)Nz+SELECT COUNT(DISTINCT dev_eui) FROM uplinksr   rb   r   s    r   device_countrg      s@    	 It||9;;C8:aII I Ire   c           	          t         5  t               5 }|j                  d| |dn
t        |      f       d d d        d d d        y # 1 sw Y   xY w# 1 sw Y   y xY w)NzaINSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.valuer   )r   r   r   r   )keyvaluer   s      r   set_metark      sY    	 8hj 8DE"3u:6	88 8 8 8 8 8s!   A"AAA	
AAc                     t               5 }|j                  d| f      j                         }d d d        r|d   S d S # 1 sw Y   xY w)Nz$SELECT value FROM meta WHERE key = ?r   rb   )ri   r   rN   s      r   get_metarm      sT    	 Gtll2SF<<DHJ 	G 3q6"d"G Gs	   ">Ac                    t        j                         j                  d      }t        d|       | r1t        d|       t        dd       t        d|       t        d|       y
t        dt	        |xs d             t        d	|       y
)z?Persist the outcome of one ingest POST for UI health surfacing.r   ingest_last_runingest_last_okingest_last_errorr   ingest_last_receivedingest_last_insertedzunknown erroringest_last_error_atN)r   nowr   rk   r   )okerrorreceivedinsertedru   s        r   record_ingestrz      ss    
,,.
!
!"5
6C$	!3'$b)'2'2$c%*B?&CD'-r   c                      t        d      t        d      t        d      xs dt        d      t               t               dS )z'Ingest health summary for API payloads.ro   rp   rq   Nrt   )last_runlast_ok
last_errorlast_error_atuplinksrY   )rm   rd   rg   rS   r   r   ingest_healthr      sA     ./,-23;t!"89>> r   )	thingpark)i  )Nr   r   )__doc__r@   osr	   	threadingr   pathlibr   environr>   __file__resolveparentr   Lockr   r   r   r!   r(   r+   rJ   rP   r]   r`   rd   rg   rk   rm   rz   r   rS   r   r   <module>r      s   2  	    JJNN&>))+22^CEF 
 innR<"$+N 0(-J
I8#.	r   