o
    Li5r                     @   s  d Z ddl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	m
Z
mZmZ ddlmZmZmZmZmZ zddlmZmZmZ W n eyQ   dZdZd	ZY nw dd
lmZmZmZ ejejeZe	de ej!edddZ"dd Z#d0ddZ$d1ddZ%dd Z&dd Z'dd Z(da)da*dd Z+dd Z,d d! Z-d"d# Z.e"/d$d%d& Z0e"j/d'd(gd)d*d+ Z1e"j/d,d-d(gd)d.d/ Z2dS )2z
SolarMon: solar dashboard (Victron, Growatt, EPEver, batteries) and temp-sensor API.
Uses MySQL shop_stats; replicates appearance of PHP SolarMon index.php.
Weather for System column from Open-Meteo (Columbia, MS).
    N)datetime)	Blueprintrender_templaterequestResponse)
MYSQL_HOST
MYSQL_PORTMYSQL_DATABASE
MYSQL_USERMYSQL_PASSWORD)WEATHER_LATWEATHER_LONWEATHER_CACHE_MINUTESg     @?@gQuV   )ensure_ota_tablesget_latest_version_strversion_lesssolarmonsolarmon_staticz/solarmon/static)static_folderstatic_url_pathc                   C   s   t jtttttt jjdS )N)hostportuserpassworddatabasecursorclass)	pymysqlconnectr   r   r
   r   r	   cursors
DictCursor r!   r!   +/var/www/html/Server/blueprints/solarmon.py	_get_conn'   s   r#   c              	   C   sR   | r|| vs| | d u s| | dkr|S zt | | W S  ttfy(   | Y S w )N )float	TypeError
ValueErrorrowkeydefaultr!   r!   r"   _float2   s   $r,   r$   c                 C   s,   | r|| vs| | d u r|S t | |  S )N)strstripr(   r!   r!   r"   _str;   s   r/   c              	   C   s   | du st | tr|  sdS zt| }W n ttfy!   Y dS w d|  kr,dkr@n dS tdtdt|d d d dS dS )u   
    Single-cell Li-ion: return bar fill 0–100 from voltage (2.5V–4.3V).
    Bar width = position in voltage range, not charge percentage.
    Ng      @g333333@r   d   g?   )	
isinstancer-   r.   r%   r'   r&   maxminround)batvr!   r!   r"   _bat_voltage_bar_pctA   s   "r8   c                 C   s   z||  }W n
 t y   Y dS w t| }|dk rd}|d }|d }|d }|dk r/dS |dk rD| d|dkr?d d	S d d	S |dk rY| d
|dkrTd d	S d d	S | d|dkred d	S d d	S )za
    Return a short, human-readable delta like '5 minutes ago', '2 hours ago', '3 days ago'.
    r$   r   <      zjust nowz minuter1   sz agoz hourz day)	Exceptioninttotal_seconds)dtnowdeltasecondsminuteshoursdaysr!   r!   r"   _human_timedeltaR   s$   """rF   c                 C   sv   z0t |  |  }|d|f | }W d   n1 sw   Y  |s)W dS t| |d W S  ty:   Y dS w )zDReturn latest firmware version string for OTA project slug, or None.z+SELECT id FROM ota_projects WHERE slug = %sNid)r   cursorexecutefetchoner   r<   )connslugcurr)   r!   r!   r"   _ota_latest_version_by_slugl   s   

rN   c                 C   s   | du rdS t | }|dkrdS |dv r|dkrdS dS |d	v r"d
S d|  kr,dkr6n n|dkr4dS dS d|  krAdkrBdS  d|  krMdkrNdS  d|  krYdkrZdS  d|  kredkrhdS  dS dS )z;Map WMO weather code to short condition string for display.Nr$   r   Clear)r1         rP   zPartly cloudyOvercast)-   0   Fog3   C   =   RainDrizzleG   M   SnowP   R   ShowersU   V   zSnow showers_   c   Thunderstorms)r=   )codecr!   r!   r"   _wmo_weathercode_to_conditions   s0   rh   c            
      C   sJ  d tt} z-tjj| ddid}tjj|dd}t|	 
 }W d   n1 s-w   Y  W n tjjtjjtjtfyF   Y dS w z|dpNi }|d	pUi }|d
p\g }|dpcg }|dpjg }i }	|ddurtt|dddnd|	d< t|dkr|d durtt|d dnd|	d< t|dkr|d durtt|d dnd|	d< tt|dkr|d nd|	d< d|	d< t|dkr|d durtt|d dnd|	d< t|dkr|d durtt|d dnd|	d< d|	d< tt|dkr|d nd|	d< d|	d< |	W S  tttfy$   Y dS w )z
    Fetch today/tomorrow weather for Columbia, MS from Open-Meteo.
    Returns dict with w_conditions, w_temp, w_tempmax, w_tempmin, wt_conditions, wt_tempmax, wt_tempmin
    or None on failure.
    zhttps://api.open-meteo.com/v1/forecast?latitude={:f}&longitude={:f}&timezone=America/Chicago&current_weather=true&temperature_unit=fahrenheit&daily=temperature_2m_max,temperature_2m_min,weathercodez
User-AgentzSolarMon/1.0)headers
   )timeoutNcurrent_weatherdailytemperature_2m_maxtemperature_2m_minweathercodetemperaturer   r1   r$   w_temp	w_tempmax	w_tempminw_conditionsw_icon
wt_tempmax
wt_tempminwt_tempwt_conditionswt_icon)formatr   r   urllibr   RequesturlopenjsonloadsreaddecodeerrorURLError	HTTPErrorJSONDecodeErrorOSErrorgetr-   r5   lenrh   
IndexErrorr&   r'   )
urlreqrespdatacwrm   maxesminscodesoutr!   r!   r"   _fetch_weather_open_meteo   sB   ,22 22"r   c                  C   sb   t  } tdurttnd}tdur$tdur$| t  }||d k r$tS t }|dur/|a| a|S )z?Return cached weather or fetch and cache. Returns dict or None.Nr   r9   )r   r@   r   r=   _weather_cache_weather_cache_timer>   r   )r@   
cache_minsrA   weatherr!   r!   r"   _get_weather_cached   s   r   c            (      C   s  i ddddddddddddddd	dd
dddddddddddddddddi ddddddddddddddddddddd dd!dd"dd#g d$dd%dd&dddddg g d'd'd(} zt  }W n tjy   |  Y S w z[i }d)D ]	}t||||< qt }|r|dd| d< |dd| d< |dd| d< |dd| d< |dd| d< |d
d| d
< |dd| d< |dd| d< |d	d| d	< |dd| d< | }|d*d+ | }|rt	|d,}|r
|d- nd| d< t
t	|d.| d< t
t	|d/| d< |d0}|d'ur|dkrzEt|}|dk r>d1| d< n5t|d2 d3 }	t|t|	d2 d3  d3 }
t||	d2 d3 d3 |
d3   d3 }|	 d4|
 d5| d6| d< W n ttfy   Y nw W d'   n	1 sw   Y  | M}|d7 | }|rt	|d}t	|d}t	|d}t	|d}t	|d}|| d< || d< || d< || d< || d< || | | | | d< W d'   n	1 sw   Y  | 9}|d8 | }|r!t	|d}t	|d}t	|d}t	|d}t	|d}|| | | | | d< W d'   n	1 s,w   Y  | %}|d9 | }|rSt	|dt	|d t	|d | d"< W d'   n	1 s^w   Y  d:d; }| <}|d< | }g }|D ]"}t|d=}t	|d>}||t
t	|d?t	|d@|||dA qy|| d#< W d'   n	1 sw   Y  | }|dB | }|rV|dCp|d$pd}t|t
tfrt|}nt| }dDD ]}||r|d't|   } nqzt|}|dE dF dG dHdI| d$< W n ttfy"   |pd| d$< Y nw t|dJ| d%< t|dK}|dLddMdNdOdP}|| d&< t|dQ| dQ< t|dR| dR< t|dS| dS< W d'   n	1 saw   Y  | }z|dT W n tjy}   Y nw |dU | }t }|dV} |D ]n}!t|!dW|!dX< |!dW}"|"d'ur|"dkrzt|"dYdZ|!d[< W n ttfy   t|"|!d[< Y nw d|!d[< |!d\}#|#rt|#||!d]< nd|!d]< t| o|!d^ot|!d^ | |!d_< | |!d`< dV|!da< q|| db< W d'   n	1 sw   Y  z| }z|dc W n tjy-   Y nw |dd | }t }|de}$|D ]|}!t|!df|!dX< |!df}"|"d'ur{|"dkr{zt|"dYdZ|!d[< W n ttfyz   t|"|!d[< Y nw d|!d[< |!dg}%|%r||%  dhk|!di< t|%||!d]< ndj|!di< d|!d]< t|$o|!d^ot|!d^ |$|!d_< |$|!d`< de|!da< qB|| dk< W d'   n	1 sw   Y  W n tjy   Y nw z_| P}|dl | }|r.|dgr	t |}t }t|dg ||d]< |dm}&t|&o|d^ot|d^ |&|d_< |&|d`< dm|da< || dn< W d'   n	1 s9w   Y  W n tjtj!fyM   Y nw z| r}|do | }|r|dgrwt |}t }t|dg ||d]< |dpd'urt|dp|dp< |dqd'urt|dq|dq< |dr}'t|'o|d^ot|d^ |'|d_< |'|d`< dr|da< || ds< W d'   n	1 sw   Y  W n tjtj!fy   Y nw W |"  n|"  w zt| d | d d-  dt| du< W | S  ttfy   d| du< Y | S w )vzTFetch all data for the main dashboard; return a dict with defaults for missing data.rt   r$   rs   rr   ru   rv   rx   rw   ry   rz   r{   vic_voltageg        currentr   vic_socvic_ttgz	No RecordGw1PGw2PGw3PEP1PEP2Psolar_powerGw1CGw2CGw3CEP1CEP2Ctotal_currentGw1LoadGw2LoadGw3Loadtotal_inv_current	batteries	shed_temp
shed_humidup_timeN)dateuserstasksconsumptiontemps_latestmoisture_latestpump_latestwifiswitch_latest)	wifitempsmoisturemeterpumpcontrol
wifiswitchzSELECT V, P, I, SOC, TTG FROM VEDirect WHERE Name = %s AND LastUpdate >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) ORDER BY id DESC LIMIT 1)z	Victron 2VgMbP?ISOCTTGInfinater:   r9   z Days z Hrs z Minaa  
                SELECT
                  (SELECT ((((Ppv1H * 10) << 16) + (Ppv1L * 10)) * .1) FROM Growatt
                   WHERE Name = 'Growatt 1' AND LastUpdate >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) ORDER BY id DESC LIMIT 1) AS Gw1P,
                  (SELECT ((((Ppv1H * 10) << 16) + (Ppv1L * 10)) * .1) FROM Growatt
                   WHERE Name = 'Growatt 2' AND LastUpdate >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) ORDER BY id DESC LIMIT 1) AS Gw2P,
                  (SELECT ((((Ppv1H * 10) << 16) + (Ppv1L * 10)) * .1) FROM Growatt
                   WHERE Name = 'Growatt 3' AND LastUpdate >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) ORDER BY id DESC LIMIT 1) AS Gw3P,
                  (SELECT ((((r3103 * 10) << 16) + (r3102 * 10)) * .001) FROM EPEver
                   WHERE Name = 'Epever 01' AND LastUpdate >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) ORDER BY id DESC LIMIT 1) AS EP1P,
                  (SELECT ((((r3103 * 10) << 16) + (r3102 * 10)) * .001) FROM EPEver
                   WHERE Name = 'Epever 02' AND LastUpdate >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) ORDER BY id DESC LIMIT 1) AS EP2P
                aW  
                SELECT
                  (SELECT Buck1Curr FROM Growatt WHERE Name = 'Growatt 1' AND LastUpdate >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) ORDER BY id DESC LIMIT 1) AS Gw1C,
                  (SELECT Buck1Curr FROM Growatt WHERE Name = 'Growatt 2' AND LastUpdate >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) ORDER BY id DESC LIMIT 1) AS Gw2C,
                  (SELECT Buck1Curr FROM Growatt WHERE Name = 'Growatt 3' AND LastUpdate >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) ORDER BY id DESC LIMIT 1) AS Gw3C,
                  (SELECT (r3105 * .01) FROM EPEver WHERE Name = 'Epever 01' AND LastUpdate >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) ORDER BY id DESC LIMIT 1) AS EP1C,
                  (SELECT (r3105 * .01) FROM EPEver WHERE Name = 'Epever 02' AND LastUpdate >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) ORDER BY id DESC LIMIT 1) AS EP2C
                a
  
                SELECT
                  (SELECT Inv_Curr FROM Growatt WHERE Name = 'Growatt 1' AND LastUpdate >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) ORDER BY id DESC LIMIT 1) AS Gw1C,
                  (SELECT Inv_Curr FROM Growatt WHERE Name = 'Growatt 2' AND LastUpdate >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) ORDER BY id DESC LIMIT 1) AS Gw2C,
                  (SELECT Inv_Curr FROM Growatt WHERE Name = 'Growatt 3' AND LastUpdate >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) ORDER BY id DESC LIMIT 1) AS Gw3C
                c                 S   s8   | dkrdS | dkrdt | d dS dt | d dS )Nr   	EqualizedzChar: rP   azDisc: )r5   )rg   r!   r!   r"   _bat_status|  s
   z)fetch_dashboard_data.<locals>._bat_statusa  
                SELECT b.Name, b.PercentCapacity, b.Voltage, b.Current
                FROM Battery b
                INNER JOIN (
                    SELECT Name, MAX(id) AS max_id
                    FROM Battery
                    WHERE LastUpdate >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)
                    GROUP BY Name
                ) t ON b.Name = t.Name AND b.id = t.max_id
                ORDER BY b.Name
                NameCurrentPercentCapacityVoltage)namesocvoltager   statuszSELECT shed_Temperature AS shed_temp, shed_Humiditity AS shed_humiditity, `up`, `date`, users, tasks FROM Environment WHERE LastUpdate >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) AND shed_Humiditity <> '' ORDER BY id DESC LIMIT 1shed_Temperature)u    °Cu   °Cz CCrg   	          z.1fu    °Fshed_humiditityupzup rD   hrC   mr   r   r   zFALTER TABLE Temps ADD COLUMN firmware_version VARCHAR(24) DEFAULT NULLaL  
                SELECT t.unit_id, t.node_name, t.temp, t.humid, t.bat, t.entry_date, t.firmware_version
                FROM Temps t
                INNER JOIN (
                    SELECT unit_id, MAX(entry_date) AS max_entry_date
                    FROM Temps
                        WHERE entry_date >= NOW() - INTERVAL 20 MINUTE
                    GROUP BY unit_id
                ) latest ON t.unit_id = latest.unit_id AND t.entry_date = latest.max_entry_date
                    WHERE t.entry_date >= NOW() - INTERVAL 20 MINUTE
                ORDER BY t.unit_id
                r   r6   bat_bar_pctz.2fz Vbat_display
entry_datelast_seen_agofirmware_versionota_newer_availableota_latest_versionota_project_slugr   zNALTER TABLE MoistureLevel ADD COLUMN firmware_version VARCHAR(24) DEFAULT NULLa  
                    SELECT m.device_id, m.node_name, m.moisture, m.battery_voltage, m.command, m.firmware_version, m.created_at
                    FROM MoistureLevel m
                    INNER JOIN (
                        SELECT device_id, MAX(id) AS max_id FROM MoistureLevel GROUP BY device_id
                    ) latest ON m.device_id = latest.device_id AND m.id = latest.max_id
                    WHERE m.created_at >= NOW() - INTERVAL 20 MINUTE
                    ORDER BY m.device_id
                    r   battery_voltage
created_ati  is_staleFr   zSELECT id, pressure_psi, pump_on, error_pressure_low, app_state, rssi, ip, mac, firmware_version, created_at FROM PumpStatus ORDER BY created_at DESC LIMIT 1r   r   zSELECT id, unit_id, node_name, temp, humid, relay_on, temp_rule_enabled, mac, ip, firmware_version, created_at FROM WifiSwitchStatus ORDER BY created_at DESC LIMIT 1relay_ontemp_rule_enabledr   r   rP   r   )#r#   r   ErrorrN   r   r   rH   rI   rJ   r,   r=   r%   r5   r&   r'   fetchallr/   appendr2   r-   r.   endswithr   replaceOperationalErrorr   r@   r8   rF   boolr   r>   dictProgrammingErrorclose)(defaultsrK   
ota_latestrL   r   rM   r)   r7   ttgrE   rD   rC   gw1gw2gw3ep1ep2c1c2c3c4c5r   rowsr   r   r   raw_tempsuffixrg   r   r@   	ota_tempsrbentry_dtota_moisturecreatedota_pump
ota_switchr!   r!   r"   fetch_dashboard_data   s  	
 !"#,






 


















 

&



.




+





r  /c                  C   s   t  } tdi | S )z;Main SolarMon dashboard (same appearance as PHP index.php).solarmon/index.htmlN)r	  )r  r   )r   r!   r!   r"   indexg  s   r
  z/moisture/deletePOST)methodsc                  C   s   t jddpi } | dpd }|stddddS zt }W n tjy0   td	d
dd Y S w zFz+| }|	d|f W d   n1 sIw   Y  |
  tdddW W |  S  tjyw   |  tdd
dd Y W |  S w |  w )z
    Delete all MoistureLevel records for a given device_id.
    Intended to be called from the SolarMon dashboard via AJAX.
    T)silent	device_idr$   z{"error": "device_id required"}i  zapplication/json)r   mimetypez!{"error": "database unavailable"}i  z.DELETE FROM MoistureLevel WHERE device_id = %sNz{"success": true}r  z{"error": "delete failed"})r   get_jsonr   r.   r   r#   r   r   rH   rI   commitr   rollback)payloadr  rK   rM   r!   r!   r"   delete_moisture_devicen  sJ   



	


r  z/tempsGETc                  C   s"  t jdkrt jnt j} | dd }| dd }| dd }| dd }| dd }| dd }| d	d }| d
pId dd pQd}t d}	zt	 }
W n t
jyn   tddd Y S w zzd}|
 .}|d|f | }|r|drtt|d  dkrt|d  }W d   n1 sw   Y  |
 }|d||	|||||||f	 W d   n1 sw   Y  |
  |rtd| dddW W |
  S tdddW W |
  S  t
jy   |
  tddd Y W |
  S w |
  w )z
    Temp-sensor API: accept id, temp, humid, mac, ip, bat, name.
    Insert reading; return '~' or '@Com:<command>~' (parameterized queries).
    r  rG   r$   temphumidmacipr6   r   r   Nr:   z%Y-%m-%d %H:%M:%S~z
text/plainr  zESELECT command FROM Temps WHERE unit_id = %s ORDER BY id DESC LIMIT 1commandr   zINSERT INTO Temps (unit_id, entry_date, temp, humid, mac, ip, bat, node_name, firmware_version) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)z@Com:)r   methodargsformr   r.   r   utcnowstrftimer#   r   r   r   rH   rI   rJ   r   r-   r  r   r  )r  unit_idr  r  r  r  r6   	node_namer   r   rK   r  rM   r)   r!   r!   r"   	temps_api  sZ   

&
	



r$  )r   )r$   )3__doc__r   osr   urllib.errorr}   urllib.requestr   flaskr   r   r   r   configr   r   r	   r
   r   r   r   r   ImportErrorblueprints.otar   r   r   pathdirnameabspath__file___BP_DIR__name__joinbpr#   r,   r/   r8   rF   rN   r   r   rh   r   r   r  router
  r  r$  r!   r!   r!   r"   <module>   sZ    

	)   

*