#!/usr/bin/env python3
import os
import sys
import ast
from ast import literal_eval
import falcon
from falcon import HTTP_200, HTTP_400, HTTP_401, HTTP_500
import json
import logging
from dotenv import load_dotenv
import calendar
import io
import datetime
from datetime import timedelta, timezone
import jwt
import psycopg2
import html
import re
import fnmatch
import traceback
import time
import pytz
from PIL import Image, ImageDraw, ImageFont
import paho.mqtt.client as mqtt
import ssl
import hashlib
import itertools
from collections import defaultdict, deque
import warnings
from io import BytesIO
import zipfile
from minio import Minio
from minio.error import S3Error
import numpy as np
import cv2
from sklearn.mixture import GaussianMixture
import openai
from openai import OpenAI
from typing import List, Tuple
import redis

st = 0
if True:

    #from scipy import interpolate
    from scipy.optimize import curve_fit
    from scipy import stats
    import pandas as pd
    #from scipy.signal import savgol_filter

EnablePlot = False #True

if EnablePlot:
    import matplotlib
    matplotlib.use('Agg')  # Set the backend before importing pyplot
    import matplotlib.pyplot as plt
    from matplotlib.colors import LinearSegmentedColormap
    import matplotlib.dates as mdates

# Configure logging
logging.basicConfig(
    level=logging.DEBUG,# .ERROR,
    format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger(__name__)

location_names = {-1:"All",0:"?",5:"Office",6:"Hallway",7:"Garage",8:"Outside",9:"Conference Room",10:"Room",34:"Kitchen",
                  56:"Bedroom",78:"Living Room",102:"Bathroom",103:"Dining Room",104:"Bathroom Main",105:"Bathroom Guest",
                  106:"Bedroom Master", 107:"Bedroom Guest", 108:"Conference Room", 109:"Basement", 110:"Attic", 200:"Other"}

Loc2Color = {"?":(0,0,0),"Office":(255,255,0),"Hallway":(128,128,128),"Garage":(128,0,0),"Outside":(0,0,0),"Conference Room":(0,0,128),
             "Room":(64,64,64),"Kitchen":(255,0,0),"Bedroom":(16,255,16),"Living Room":(160,32,240),"Bathroom":(0,0,255),
             "Dining Room":(255,128,0),"Bathroom Main":(16,16,255), "Bedroom Master":(0,255,0),"Bathroom Guest":(32,32,255),
             "Bedroom Guest":(32,255,32), "Basement":(64,64,64), "Attic":(255,165,0), "Other":(192,192,192)}

Loc2Color = {"Bedroom":((16,255,16),0),"Bedroom Master":((0,255,0),0),"Bedroom Guest":((32,255,32),0),"Bathroom":((0,0,255),1),
             "Bathroom Main":((16,16,255),1),"Bathroom Guest":((32,32,255),1),"Kitchen":((255,0,0),2),"Dining Room":((255,128,0),3),
             "Office":((255,255,0),4),"Conference Room":((0,0,128),5),"Room":((64,64,64),6),"Living Room":((160,32,240),7),"Hallway":((128,128,128),8),
             "Garage":((128,0,0),9),"Basement":((64,64,64), 10),"Attic":((255,165,0), 11),"Other":((192,192,192),12),"?":((0,0,0),13),"Outside":((0,0,0),14)}


s_table = ["temperature", "humidity", "pressure", "light", "radar", "voc0", "voc1", "voc2", "voc3", "voc4", "voc5", "voc6", "voc7", "voc8", "voc9"] # derived
smells_table = ["s0", "s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9"] # derived

Consolidataed_locations = {"?":"Room","Office":"Office","Hallway":"Hallway","Garage":"Garage","Outside":"Outside","Conference Room":"Office",
             "Room":"Room","Kitchen":"Kitchen","Bedroom":"Bedroom","Living Room":"Living Room","Bathroom Guest":"Bathroom",
             "Dining Room":"Dining Room","Bathroom":"Bathroom", "Bathroom Main":"Bathroom","Bedroom Master":"Bedroom",
             "Bedroom Guest":"Bedroom", "Basement":"Basement", "Attic":"Attic", "Other":"Room"}

AveragePercentPerLocation = {"Bedroom":[29, 37.5], "Bathroom":[2, 4], "Office":[10, 40],"Hallway":[0.1, 0.2],"Garage":[2, 3],"Outside":[5, 10],
             "Room":[5, 10],"Kitchen":[5, 12.5], "Living Room":[5, 10],
             "Dining Room":[5, 10], "Basement":[0, 0.2], "Attic":[0, 0.2]}

location_indexes = {}

for i in location_names:
    location_indexes[location_names[i]] = i



# HTTP Status codes
HTTP_200 = falcon.HTTP_200
HTTP_201 = falcon.HTTP_201
HTTP_400 = falcon.HTTP_400
HTTP_401 = falcon.HTTP_401
HTTP_404 = falcon.HTTP_404
HTTP_500 = falcon.HTTP_500

load_dotenv()

DB_NAME = os.getenv('DB_NAME')
DB_USER = os.getenv('DB_USER')
DB_PASSWORD = os.getenv('DB_PASSWORD')
DB_HOST = os.getenv('DB_HOST')
DB_PORT = os.getenv('DB_PORT')
MINIO_ACCESS_KEY = os.getenv('MINIO_ACCESS_KEY')
MINIO_SECRET_KEY = os.getenv('MINIO_SECRET_KEY')
MINIO_HOST = os.getenv('MINIO_HOST')
MINIO_PORT = os.getenv('MINIO_PORT')
DAILY_MAPS_BUCKET_NAME = os.getenv('DAILY_MAPS_BUCKET_NAME')
JWT_SECRET = os.getenv('JWT_SECRET')
MASTER_ADMIN = os.getenv('MASTER_ADMIN')
MASTER_PS = os.getenv('MASTER_PS')

OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
model_engine = os.getenv('OPENAI_API_MODEL_ENGINE')

# Redis Configuration
REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
REDIS_PORT = int(os.getenv('REDIS_PORT', 6379))
REDIS_DB = int(os.getenv('REDIS_DB', 0))
REDIS_PASSWORD = os.getenv('REDIS_PASSWORD', None)

redis_host = os.getenv('REDIS_HOST', '192.168.68.70')
redis_host = '192.168.68.70'

# SMS/Telnyx Configuration 
OPT_IN_KEYWORD = os.environ.get("OPT_IN_KEYWORD", "WELLNUOJOIN")
TELNYX_PUBLIC_KEY = os.environ.get("TELNYX_PUBLIC_KEY") # Needed for signature validation

use_pdb = True
debug = False
debug_string = ""
logger.debug(f"Environment variables: {os.environ}")
filesDir = "/home/app/well_web_storage" #os.path.dirname(os.path.realpath(__file__))
min_io_address = MINIO_HOST + ":" + MINIO_PORT

miniIO_blob_client = Minio(min_io_address, access_key=MINIO_ACCESS_KEY, secret_key=MINIO_SECRET_KEY, secure=False)

user_id_2_user = {}
smell_min = 1
no_smell = 102400000
smell_max =  no_smell - 1
sensor_legal_values = {"radar": (0,1000, 1), "co2": (smell_min, smell_max, 31), "humidity": (1,99, 31), "light": (0, 4095, 1),
                       "pressure": (0, 10000, 5), "temperature": (1, 60, 31), "voc": (smell_min, smell_max, 31), "voc0": (smell_min, smell_max, 31),
                       "voc1": (smell_min, smell_max, 31), "voc2": (smell_min, smell_max, 31), "voc3": (smell_min, smell_max, 31), "voc4": (smell_min, smell_max, 31),
                       "voc5": (smell_min, smell_max, 31), "voc6": (smell_min, smell_max, 31), "voc7": (smell_min, smell_max, 31), "voc8": (smell_min, smell_max, 31), "voc9": (smell_min, smell_max, 31),
                       "s0": (smell_min, smell_max, 31), "s1": (smell_min, smell_max, 31), "s2": (smell_min, smell_max, 31), "s3": (smell_min, smell_max, 31), "s4": (smell_min, smell_max, 31),
                       "s5": (smell_min, smell_max, 31), "s6": (smell_min, smell_max, 31), "s7": (smell_min, smell_max, 31), "s8": (smell_min, smell_max, 31), "s9": (smell_min, smell_max, 31)}

smell_legal_values = {"s0": (smell_min, smell_max, 31), "s1": (smell_min, smell_max, 31), "s2": (smell_min, smell_max, 31), "s3": (smell_min, smell_max, 31), "s4": (smell_min, smell_max, 31),
                       "s5": (smell_min, smell_max, 31), "s6": (smell_min, smell_max, 31), "s7": (smell_min, smell_max, 31), "s8": (smell_min, smell_max, 31), "s9": (smell_min, smell_max, 31)}


def GetRedisInt(key_name):
    try:
        result = int(redis_conn.get(key_name).decode('utf-8'))
    except:
        result = None
    return result

def GetRedisFloat(key_name):
    try:
        result = float(redis_conn.get(key_name).decode('utf-8'))
    except:
        result = None

    return result

def GetRedisString(key_name):
    try:
        result = redis_conn.get(key_name).decode('utf-8')
    except:
        result = None
    return result

def GetRedisMap(key_name):
    try:
        result_bytes = redis_conn.hgetall(key_name)
        result = {k.decode('utf-8'): v.decode('utf-8') for k, v in result_bytes.items()}
    except:
        result = {}
    return result



def read_file(file_name, source = "LOCAL", type_ = "TEXT", bucket_name="daily-maps"):

    blob_data = ""
    if source == "MINIO":
        blob_data = ReadObjectMinIO(bucket_name, file_name)
    elif source == "LOCAL":
        login_file = os.path.join(filesDir, file_name)
        login_file = login_file.replace("\\","/")
        logger.debug(f"Full file path: {login_file}")
        logger.debug(f"File exists: {os.path.exists(login_file)}")
        #print(login_file)
        if type_ == "TEXT":
            with open(login_file, encoding="utf8") as f:
                blob_data = f.read()
        else:
            with open(login_file, 'rb') as f:
                blob_data = f.read()

    elif source == "AZURE":
        try:
            blob_data = ""#container_client.download_blob(file_name).readall()
        except Exception as err:
            logger.error("Not reading Azure blob "+str(err))
            blob_data = ""
        return blob_data
    else:
        pass
    return blob_data


def match_with_wildcard(string, pattern):
    return fnmatch.fnmatchcase(string, pattern)

def extract_differing_part(string, pattern):
    regex_pattern = re.escape(pattern).replace(r'\*', r'(.+)')
    match = re.match(regex_pattern, string)
    if match:
        return match.group(1)
    else:
        return None

def get_db_connection():
    return psycopg2.connect(dbname=DB_NAME, user=DB_USER, password=DB_PASSWORD, host=DB_HOST, port=DB_PORT)


def generate_token(username):
    expiration = datetime.datetime.now(timezone.utc) + timedelta(hours=24)
    token = jwt.encode({"username": username, "exp": expiration}, JWT_SECRET, algorithm="HS256")
    return token


def verify_token(token):
    try:
        payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
        return payload
    except jwt.ExpiredSignatureError:
        return None
    except jwt.InvalidTokenError:
        return None

def SaveObjectInBlob(file_name, obj):
    """
    Saves a Python object to MinIO blob storage using JSON serialization

    Args:
        file_name (str): Name of the file to save in blob storage
        obj: Python object to serialize and save
    """
    try:
        # Convert object to JSON string
        json_str = json.dumps(obj)
        # Convert string to bytes
        json_bytes = json_str.encode('utf-8')

        # Save to MinIO
        miniIO_blob_client.put_object(
            DAILY_MAPS_BUCKET_NAME,
            file_name,
            io.BytesIO(json_bytes),
            len(json_bytes)
        )
        return True
    except Exception as e:
        logger.error(f"Error saving object to blob: {traceback.format_exc()}")
        return False

def ReadObjectMinIO(bucket_name, file_name):
    try:
        # Retrieve the object data
        response = miniIO_blob_client.get_object(bucket_name, file_name)

        # Read the data from response
        data_bytes = response.read()

        # Convert bytes to string and then load into a dictionary
        data_string = data_bytes.decode('utf-8')

        # Don't forget to close the response
        response.close()
        response.release_conn()

        return data_string

    except S3Error as e:
        logger.error(f"An error occurred while reading {file_name}: {e}")
        return None
    except:
        logger.error(f"An error occurred while decoding {file_name}")
        return None

#def ReadObjectMinIO(bucket_name, file_name):
    #try:
        ## Retrieve the object data
        #response = miniIO_blob_client.get_object(bucket_name, file_name)

        ## Read the data from response
        #data_bytes = response.read()

        ## Convert bytes to string and then load into a dictionary
        #data_string = data_bytes.decode('utf-8')

        ## Don't forget to close the response
        #response.close()
        #response.release_conn()

        #return data_string

    #except S3Error as e:
        #logger.error(f"An error occurred: {e}")
        #return None


def package_response_C(payload, status_code=HTTP_200):
    """Package response in a standard format"""
    if status_code == HTTP_200:
        return {"status": "success", "data": payload}
    else:
        return {"status": "error", "message": payload, "code": status_code}

def package_response(content, status=falcon.HTTP_200):
    """
    Format the HTTP response.

    :param content: The content to be returned in the response.
    :param status: HTTP status code (default is 200 OK).
    :return: A dictionary containing the formatted response.
    """
    if isinstance(content, str):
        # If content is a string, try to parse it as JSON
        try:
            response = json.loads(content)
        except json.JSONDecodeError:
            # If it's not valid JSON, use it as message
            response = {"message": content}
    elif isinstance(content, dict):
        # If content is a dictionary, serialize it with datetime handling
        try:
            # First serialize to JSON string with datetime handling
            json_str = json.dumps(content, default=datetime_handler)
            # Then parse back to dict
            response = json.loads(json_str)
        except TypeError as e:
            response = {"message": f"Serialization error: {str(e)}"}
    else:
        # For any other type, convert to string and use as message
        response = {"message": str(content)}

    # Add status code to the response
    response["status"] = status

    # Handle specific status codes
    if status == falcon.HTTP_400:
        response["error"] = "Bad Request"
    elif status == falcon.HTTP_401:
        response["error"] = "Unauthorized"
    elif status == falcon.HTTP_500:
        response["error"] = "Internal Server Error"

    return response

def GetPriviledges(conn, user_name, password):
    sql = "SELECT key, access_to_deployments, user_id FROM public.person_details WHERE user_name = '" + user_name + "'"

    with conn.cursor() as cur:
        cur.execute(sql)
        result = cur.fetchall()#cur.fetchone()
        if  result != None:
            if result[0][0] == password:
                return result[0][1], result[0][2]
            else:
                return "0", "0"
        else:
            return "0", "0"

def GetPriviledgesOnly(user):
    with get_db_connection() as conn:
        if isinstance(user, (int)) or user.isdigit():
            sql = "SELECT access_to_deployments FROM public.person_details WHERE user_id = " + user
        else:
            sql = "SELECT access_to_deployments FROM public.person_details WHERE user_name = '" + user + "'"

        with conn.cursor() as cur:
            cur.execute(sql)
            result = cur.fetchall()#cur.fetchone()
            if  result != None:
                return result[0][0]
            else:
                return "0"

def GetUserId(user_name):
    with get_db_connection() as conn:
        sql = "SELECT user_id FROM public.person_details WHERE user_name = '" + user_name + "'"

        with conn.cursor() as cur:
            cur.execute(sql)
            result = cur.fetchall()#cur.fetchone()
            if  result != None:
                return result[0][0]
            else:
                return "0"

def GetNameFromUserId(user_id):
    with get_db_connection() as conn:
        sql = f"SELECT user_name, first_name, last_name FROM public.person_details WHERE user_id = {user_id}"

        with conn.cursor() as cur:
            cur.execute(sql)
            result = cur.fetchall()#cur.fetchone()
            if  result != None:
                return result[0]
            else:
                return None



def ListDeployments(priviledges, user_id):

    global user_id_2_user

    conn = get_db_connection()

    if priviledges == "-1":
        sql = "SELECT * FROM public.deployments ORDER BY deployment_id ASC;"
    else:
        sql = f"SELECT * FROM public.deployments WHERE deployment_id IN ({priviledges}) OR user_edit = {user_id} ORDER BY deployment_id ASC;"

    try:

        with conn.cursor() as cur:
            cur.execute(sql)
            result = cur.fetchall()#cur.fetchone()
            if result == None:
                complete_result = []
            else:
                deployment_ids = []
                deployment_records_dict = {}
                for record in result:
                    deployment_id = record[0]
                    deployment_ids.append(deployment_id)
                    deployment_records_dict[deployment_id] = record

            sql = f"SELECT * FROM public.deployment_details WHERE deployment_id IN ({','.join(map(str, deployment_ids))}) ORDER BY deployment_id ASC;"
            cur.execute(sql)
            details_result = cur.fetchall()

            beneficiary_ids = []

            for record_details in details_result:
                if record_details[1] != None and record_details[1] not in beneficiary_ids:
                    beneficiary_ids.append(record_details[1])

            sql = f"SELECT * FROM public.person_details WHERE user_id IN ({','.join(map(str, beneficiary_ids))});"


            cur.execute(sql)
            user_id_2_user = {}
            users = cur.fetchall()#cur.fetchone()
            for usr_record in users:
                user_id_2_user[usr_record[0]] = usr_record

            complete_result = []
            if details_result != None:
                for record_details in details_result:
                    deployment_record = deployment_records_dict[record_details[0]]
                    complete_record = {'deployment_id': record_details[0], 'beneficiary_id': record_details[1], 'caretaker_id': record_details[2],
                                       'owner_id': record_details[3], 'installer_id': record_details[4],
                                       'address_street': record_details[6], 'address_city': record_details[7], 'address_zip': record_details[8],
                                       'address_state': record_details[9], 'address_country': record_details[10],
                                       'devices': record_details[5], 'wifis': record_details[11], 'persons': deployment_record[4], 'gender': deployment_record[5],
                                       'race': deployment_record[6], 'born': deployment_record[7], 'pets': deployment_record[8], 'time_zone': deployment_record[3]
                                       }
                    complete_result.append(complete_record)
    except:
        logger.debug(f"Error: {traceback.format_exc()}")
    return complete_result

def ListCaretakers(privileges, user_name):

    conn = get_db_connection()
    if privileges == "-1":
        sql = "SELECT * FROM public.person_details WHERE role_ids LIKE '%2%' ORDER BY last_name;" #2 is caretaker

        with conn.cursor() as cur:
            cur.execute(sql)
            result = cur.fetchall()#cur.fetchone()
            if result == None:
                result = []
    else:
        #we need to check if
        sql = f"SELECT * FROM public.person_details WHERE user_name = '{user_name}';" #2 is caretaker

        with conn.cursor() as cur:
            cur.execute(sql)
            result = cur.fetchall()#cur.fetchone()
            if result == None:
                result = []

        pass
    return result

def ListBeneficiaries(privilidges, user_info):

    conn = get_db_connection()
    with conn.cursor() as cur:

        if (privilidges == "-1"):
            sql = "SELECT * FROM public.person_details WHERE role_ids LIKE '%1%' ORDER BY last_name;" #1 is beneficiary
        else:
            #we need to find beneficiaries from list of deployments
            sql = f"SELECT beneficiary_id FROM public.deployment_details WHERE deployment_id IN ({privilidges}) ORDER BY deployment_id ASC;"
            cur.execute(sql)
            result1 = cur.fetchall()#cur.fetchone()
            if result1 == None:
                result = []
                return result
            beneficiaries = ",".join(str(x[0]) for x in result1)
            sql = f"SELECT * FROM public.person_details WHERE user_id IN ({beneficiaries}) OR user_edit = {user_info} AND role_ids LIKE '%1%' ORDER BY last_name;" #1 is beneficiary
            logger.debug(f"sql= {sql}")

        cur.execute(sql)
        result = cur.fetchall()#cur.fetchone()
        if result == None:
            result = []

    return result

def UserDetails(user_id):

    conn = get_db_connection()

    sql = "SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'person_details';"
    with conn.cursor() as cur:
        cur.execute(sql)
        columns_names = cur.fetchall()

    sql = "SELECT * FROM public.person_details WHERE user_id = "+user_id

    caretaker_record = {}
    with conn.cursor() as cur:
        cur.execute(sql)
        result = cur.fetchone() #cur.fetchall()
        if result != None:
            cnt = 0
            for field in columns_names:
                caretaker_record[field[0]] = result[cnt]
                cnt += 1

    return caretaker_record

def DeviceDetails(mac):

    conn = get_db_connection()

    sql = "SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'devices';"
    with conn.cursor() as cur:
        cur.execute(sql)
        columns_names = cur.fetchall()

    sql = "SELECT * FROM public.devices WHERE device_mac = '" + mac + "'"

    device_record = {}
    with conn.cursor() as cur:
        cur.execute(sql)
        result = cur.fetchone() #cur.fetchall()
        if result != None:
            cnt = 0
            for field in columns_names:
                device_record[field[0]] = result[cnt]
                cnt += 1

    return device_record

def GetDeviceDetailsSingle(device_id):

    conn = get_db_connection()

    sql = "SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'devices';"
    with conn.cursor() as cur:
        cur.execute(sql)
        columns_names = cur.fetchall()

    sql = "SELECT * FROM public.devices WHERE device_id = '" + device_id + "'"

    device_record = {}
    with conn.cursor() as cur:
        cur.execute(sql)
        result = cur.fetchone() #cur.fetchall()
        if result != None:
            cnt = 0
            for field in columns_names:
                device_record[field[0]] = result[cnt]
                cnt += 1

    return device_record


def DeploymentDetails(deployment_id):

    deployment_record = {}

    conn = get_db_connection()

    with conn.cursor() as cur:

        sql = "SELECT * FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'deployments';"
        cur.execute(sql)
        columns_names = cur.fetchall()

        sql = "SELECT * FROM public.deployments WHERE deployment_id = '" + deployment_id + "'"

        cur.execute(sql)
        result = cur.fetchone() #cur.fetchall()
        if result != None:
            cnt = 0
            for field in columns_names:
                deployment_record[field[3]] = result[cnt]
                cnt += 1

        sql = "SELECT * FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'deployment_details';"
        cur.execute(sql)
        columns_names = cur.fetchall()

        sql = "SELECT * FROM public.deployment_details WHERE deployment_id = '" + deployment_id + "'"

        cur.execute(sql)
        result = cur.fetchone() #cur.fetchall()
        if result != None:
            cnt = 0
            for field in columns_names:
                deployment_record[field[3]] = result[cnt]
                cnt += 1


    return deployment_record

def ValidUser(user_name, password):


    if use_pdb:
        with get_db_connection() as db_conn:
            priviledges, user_id= GetPriviledges(db_conn, user_name, password)
            return priviledges, user_id

    else:
        pass
        #container = GetReference("/MAC")
        #try:
            ## We can do an efficient point read lookup on partition key and id
            ##response = container.read_item(item="64B708896BD8_temperature_2024-01-01_00", partition_key="64B708896BD8") #OK
            ##items = query_items(container, '64B708896BD8') #Too slow
            ##AddToLog("1!")
            #privileges = GetCaretakers(container, email, password)
            #return privileges

        #except Exception as err:
            #AddToLog("Error !1 "+str(err))


def SelectOption(html_code, select_id, selected_item):
    """
    Modifies HTML code to set the selected attribute for a specific option in a select element.

    Args:
        html_code (str): Original HTML code
        select_id (str): ID of the select element to modify
        selected_item (str or int): Value of the option to be selected

    Returns:
        str: Modified HTML code with the selected attribute added
    """
    # Convert selected_item to string for comparison
    selected_item = str(selected_item)

    # Find the select element with the given ID
    select_pattern = rf'<select[^>]*id=[\'"]?{select_id}[\'"]?[^>]*>(.*?)</select>'
    select_match = re.search(select_pattern, html_code, re.IGNORECASE | re.DOTALL)

    if not select_match:
        return html_code  # Return unchanged if select element not found

    select_content = select_match.group(0)
    select_content_orig = select_content
    # Remove any existing selected attributes
    select_content = re.sub(r'\s+selected(?=[>\s])', '', select_content, flags=re.IGNORECASE)

    # Add selected attribute to the matching option
    def replace_option(match):
        value = re.search(r'value=[\'"]?([^\'">\s]+)', match.group(0))
        if value and value.group(1) == selected_item:
            # Add selected attribute before the closing >
            return match.group(0).rstrip('>') + ' selected>'
        return match.group(0)

    modified_select = re.sub(
        r'<option[^>]*>',
        replace_option,
        select_content
    )

    # Replace the original select element with the modified one
    return html_code.replace(select_content_orig, modified_select)

def FillFields(blob_data, record, form_type):
    """
    Fill in the input fields in the HTML blob_data with values from the caretaker dictionary.

    :param blob_data: str - The initial HTML string containing empty or placeholder input fields.
    :param caretaker: dict - The dictionary containing values to populate the fields.
    :return: str - The HTML string with the input fields filled with the appropriate values.
    """
    # Ensure blob_data is a string
    #blob_data = str(blob_data)

    # Populate the fields
    for field in record:
        logger.debug(f"field= {field}")
        if field == "user_id":
            if record[field] is not None:
                escaped_string = html.escape(str(record[field]))
                # Create a regex pattern to match the span with specific id
                pattern = rf'(<span[^>]+id="editing_user_id"[^>]*>)([^<]*)(</span>)'
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)}{escaped_string}{m.group(3)}', blob_data)
        elif field == "deployment_id":
            if record[field] is not None:
                escaped_string = html.escape(str(record[field]))
                # Create a regex pattern to match the span with specific id
                pattern = rf'(<span[^>]+id="editing_deployment_id"[^>]*>)([^<]*)(</span>)'
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)}{escaped_string}{m.group(3)}', blob_data)
        elif field == "device_id":
            if record[field] is not None:
                escaped_string = html.escape(str(record[field]))
                # Create a regex pattern to match the span with specific id
                pattern = rf'(<span[^>]+id="editing_device_id"[^>]*>)([^<]*)(</span>)'
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)}{escaped_string}{m.group(3)}', blob_data)
        elif field == "user_name":
            if record[field] != None:
                escaped_string = html.escape(record[field])
                pattern = rf'(<input[^>]+id="new_user_name"[^>]+value=")[^"]*(")'
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)}{escaped_string}{m.group(2)}', blob_data)

                # Add value attribute if it does not exist
                pattern = rf'(<input[^>]+id="new_user_name"[^>]*)(>)'
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)} value="{escaped_string}"{m.group(2)}', blob_data)

        elif field == "location":
            if record[field] != None:
                blob_data = SelectOption(blob_data, 'location', record[field])

        elif field == "gender":
            if record[field] != None:
                blob_data = SelectOption(blob_data, 'gender', record[field])

        elif field == "race":
            if record[field] != None:
                blob_data = SelectOption(blob_data, 'race', record[field])

        elif field == "time_zone_s":
            if record[field] != None:
                blob_data = SelectOption(blob_data, 'time_zone_s', record[field])

        elif field == "time_edit" or field == "user_edit":
            pass
        else:
            if record[field] != None:
                escaped_string = html.escape(str(record[field]))
                pattern = rf'(<input[^>]+id="{field}"[^>]+value=")[^"]*(")'
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)}{escaped_string}{m.group(2)}', blob_data)

                # Add value attribute if it does not exist
                pattern = rf'(<input[^>]+id="{field}"[^>]*)(>)'
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)} value="{escaped_string}"{m.group(2)}', blob_data)

    return blob_data

def StoreThresholds2DB(device_id, TR, BR, TLIFE, BLIFE):

    #print('\nCreating create_caretaker\n')
    # Create a caretaker object. This object has nested properties and various types including numbers, DateTimes and strings.
    # This can be saved as JSON as is without converting into rows/columns.
    conn = get_db_connection()
    cur = conn.cursor()

    if device_id == None or device_id == 0:
        return 1

    try:


        sql = f"""
        UPDATE public.devices
        SET
            radar_threshold = '[{TR},{BR},{TLIFE},{BLIFE}]'
        WHERE device_id = {device_id};
        """

        logger.debug(f"sql= {sql}")
        # Execute update query
        print(sql)
        cur.execute(sql)

        # Commit the changes to the database
        conn.commit()

        # Close the cursor and connection
        cur.close()
        conn.close()
        logger.debug("Written/updated!")
        return 1
    except Exception as err:
        AddToLog(traceback.format_exc())
        return 0


def StoreBeneficiary2DB(parameters, editing_user_id, user_id):

    #print('\nCreating create_caretaker\n')
    # Create a caretaker object. This object has nested properties and various types including numbers, DateTimes and strings.
    # This can be saved as JSON as is without converting into rows/columns.
    conn = get_db_connection()
    cur = conn.cursor()
    error_string = ""
    if editing_user_id == None or editing_user_id == "":
        editing_user_id = "0"

    try:

        current_utc_time = datetime.datetime.now(timezone.utc)

        # Convert to epoch time
        current_epoch_time = current_utc_time.timestamp()

        if editing_user_id != "0":
            sql = f"""
            UPDATE public.person_details
            SET
                email = '{CleanObject(parameters.get('email'))}',
                user_name = '{CleanObject(parameters.get('new_user_name'))}',
                first_name = '{CleanObject(parameters.get('first_name'))}',
                last_name = '{CleanObject(parameters.get('last_name'))}',
                address_street = '{CleanObject(parameters.get('address_street'))}',
                address_city = '{CleanObject(parameters.get('address_city'))}',
                address_zip = '{CleanObject(parameters.get('address_zip'))}',
                address_state = '{CleanObject(parameters.get('address_state'))}',
                address_country = '{CleanObject(parameters.get('address_country'))}',
                time_edit = {current_epoch_time},
                user_edit = {user_id},
                role_ids = '{CleanObject(parameters.get('role_ids'))}',
                phone_number = '{CleanObject(parameters.get('phone_number'))}',
                picture = '{CleanObject(parameters.get('picture'))}',
                key = '{CleanObject(parameters.get('key'))}'
            WHERE user_id = {editing_user_id};  -- replace 34 with the actual person_id you want to update
            """

        else:
            sql = f"""
            INSERT INTO public.person_details
            (role_ids, email, user_name, first_name, last_name, address_street, address_city, address_zip, address_state, address_country, time_edit, user_edit, phone_number, picture, key)
            VALUES
            ('{CleanObject(parameters.get('role_ids'))}', '{CleanObject(parameters.get('email'))}', '{CleanObject(parameters.get('new_user_name'))}',
            '{CleanObject(parameters.get('first_name'))}', '{CleanObject(parameters.get('last_name'))}', '{CleanObject(parameters.get('address_street'))}',
            '{CleanObject(parameters.get('address_city'))}', '{CleanObject(parameters.get('address_zip'))}', '{CleanObject(parameters.get('address_state'))}',
            '{CleanObject(parameters.get('address_country'))}', {current_epoch_time}, {user_id}, '{CleanObject(parameters.get('phone_number'))}',
            '{CleanObject(parameters.get('picture'))}', '{CleanObject(parameters.get('key'))}');
            """
        logger.debug(f"sql= {sql}")
        # Execute update query
        cur.execute(sql)

        # Commit the changes to the database
        conn.commit()

        # Close the cursor and connection
        cur.close()
        conn.close()

        AddToLog("Written/updated!")
        return 1, error_string
    except Exception as err:
        error_string = traceback.format_exc()
        AddToLog(error_string)
        return 0, error_string


def DeleteRecordFromDB(form_data):

    caretaker = form_data['user_name']
    privileges = GetPriviledgesOnly(caretaker)

    if privileges != "-1":
        AddToLog("Forbidden!")
        return 0

    conn = get_db_connection()
    cur = conn.cursor()
    function = form_data.get('function')
    if function == "deployment_delete":
        user_id = form_data['user_id']
        editing_deployment_id = form_data['editing_deployment_id']
        priviledges = form_data['priviledges']
        if editing_deployment_id == None or editing_deployment_id == "" or editing_deployment_id == "0":
            AddToLog("deployment_id is not defined")
            cur.close()
            conn.close()
            return 0
        try:
            if  user_id == "-1":
                sql = f"""
                DELETE FROM public.deployments WHERE deployment_id = {editing_deployment_id}
                """
                logger.debug(f"sql= {sql}")
                # Execute update query
                cur.execute(sql)

                sql = f"""
                DELETE FROM public.deployment_details WHERE deployment_id = {editing_deployment_id}
                """
                logger.debug(f"sql= {sql}")
                # Execute update query
                cur.execute(sql)

                # Commit the changes to the database
                conn.commit()

                # Close the cursor and connection
                cur.close()
                conn.close()

                AddToLog("Deleted!")
                return 1
            else:
                #lets check if user_edit = user_id
                sql = f"SELECT user_edit FROM public.deployments WHERE deployment_id = '{editing_deployment_id}'"
                cur.execute(sql)
                result = cur.fetchone()
                if priviledges != "-1":
                    if result[0] != int(user_id):
                        cur.close()
                        conn.close()
                        return 0

                sql = f"""
                DELETE FROM public.deployments WHERE deployment_id = {editing_deployment_id}
                """
                logger.debug(f"sql= {sql}")
                # Execute update query
                cur.execute(sql)

                sql = f"""
                DELETE FROM public.deployment_details WHERE deployment_id = {editing_deployment_id}
                """
                logger.debug(f"sql= {sql}")
                # Execute update query
                cur.execute(sql)

                # Commit the changes to the database
                conn.commit()

                # Close the cursor and connection
                cur.close()
                conn.close()

                AddToLog("Deleted!")
                return 1

        except Exception as err:
            AddToLog(traceback.format_exc())
            cur.close()
            conn.close()
            return 0

    elif function == "device_delete":
        user_id = form_data['user_id']
        editing_device_id = form_data['editing_device_id']
        priviledges = form_data['priviledges']
        if editing_device_id == None or editing_device_id == "" or editing_device_id == "0":
            AddToLog("editing_device_id is not defined")
            cur.close()
            conn.close()
            return 0
        try:
            if user_id == "-1":
                sql = f"""
                DELETE FROM public.deployments WHERE device_id = {editing_device_id}
                """
                logger.debug(f"sql= {sql}")
                # Execute update query
                cur.execute(sql)

                conn.commit()

                # Close the cursor and connection
                cur.close()
                conn.close()

                AddToLog("Deleted!")
                return 1
            #else:
                ##lets check if user_edit = user_id
                #sql = f"SELECT user_edit FROM public.deployments WHERE deployment_id = '{editing_deployment_id}'"
                #cur.execute(sql)
                #result = cur.fetchone()
                #if priviledges != "-1":
                    #if result[0] != int(user_id):
                        #cur.close()
                        #conn.close()
                        #return 0

                #sql = f"""
                #DELETE FROM public.deployments WHERE device_id = {editing_device_id}
                #"""
                #logger.debug(f"sql= {sql}")
                ## Execute update query
                #cur.execute(sql)

                #conn.commit()

                ## Close the cursor and connection
                #cur.close()
                #conn.close()

                #AddToLog("Deleted!")
                #return 1

        except Exception as err:
            AddToLog(traceback.format_exc())
            cur.close()
            conn.close()
            return 0
    else:

        #user_id = form_data['user_id']
        editing_user_id = form_data['delete_user_id']
        if editing_user_id == None or editing_user_id == "" or editing_user_id == "0":
            AddToLog("user_id is not defined")
            cur.close()
            conn.close()
            return 0
        try:
            if privileges == "-1": #user_id == "-1":
                sql = f"""
                DELETE FROM public.person_details WHERE user_id = {editing_user_id}
                """
                logger.debug(f"sql= {sql}")
                # Execute update query
                cur.execute(sql)

                # Commit the changes to the database
                conn.commit()

                # Close the cursor and connection
                cur.close()
                conn.close()

                AddToLog("Deleted!")
                return 1
            else:
                #lets check if user_edit = user_id
                sql = f"SELECT user_edit FROM public.person_details WHERE user_id = '{editing_user_id}'"
                cur.execute(sql)
                result = cur.fetchone()
                if result[0] != int(user_id):
                    cur.close()
                    conn.close()
                    return 0

                sql = f"""
                DELETE FROM public.person_details WHERE user_id = {editing_user_id}
                """
                logger.debug(f"sql= {sql}")
                # Execute update query
                cur.execute(sql)

                # Commit the changes to the database
                conn.commit()

                # Close the cursor and connection
                cur.close()
                conn.close()

                AddToLog("Deleted!")
                return 1

        except Exception as err:
            AddToLog(traceback.format_exc())
            cur.close()
            conn.close()
            return 0

def StoreCaretaker2DB(parameters, editing_user_id, user_id):

    #print('\nCreating create_caretaker\n')
    # Create a caretaker object. This object has nested properties and various types including numbers, DateTimes and strings.
    # This can be saved as JSON as is without converting into rows/columns.
    conn = get_db_connection()
    cur = conn.cursor()

    if editing_user_id == None or editing_user_id == "":
        editing_user_id = "0"

    try:

        current_utc_time = datetime.datetime.now(timezone.utc)

        # Convert to epoch time
        current_epoch_time = current_utc_time.timestamp()

        if editing_user_id != "0":
            sql = f"""
            UPDATE public.person_details
            SET
                role_ids = '{parameters.get('role_ids')}',
                access_to_deployments = '{parameters.get('access_to_deployments')}',
                email = '{parameters.get('email')}',
                user_name = '{parameters.get('new_user_name')}',
                first_name = '{parameters.get('first_name')}',
                last_name = '{parameters.get('last_name')}',
                address_street = '{parameters.get('address_street')}',
                address_city = '{parameters.get('address_city')}',
                address_zip = '{parameters.get('address_zip')}',
                address_state = '{parameters.get('address_state')}',
                address_country = '{parameters.get('address_country')}',
                time_edit = {current_epoch_time},
                user_edit = {user_id},
                phone_number = '{parameters.get('phone_number')}',
                picture = '{parameters.get('picture')}',
                key = '{parameters.get('key')}'
            WHERE user_id = {editing_user_id};  -- replace 34 with the actual person_id you want to update
            """

        else:
            sql = f"""
            INSERT INTO public.person_details
            (role_ids, access_to_deployments, email, user_name, first_name, last_name, address_street, address_city, address_zip, address_state, address_country, time_edit, user_edit, phone_number, picture, key)
            VALUES
            ('{parameters.get('role_ids')}', '{parameters.get('access_to_deployments')}', '{parameters.get('email')}', '{parameters.get('new_user_name')}', '{parameters.get('first_name')}',
            '{parameters.get('last_name')}', '{parameters.get('address_street')}', '{parameters.get('address_city')}', '{parameters.get('address_zip')}', '{parameters.get('address_state')}',
            '{parameters.get('address_country')}', {current_epoch_time}, {user_id}, '{parameters.get('phone_number')}', '{parameters.get('picture')}', '{parameters.get('key')}');
            """
        logger.debug(f"sql= {sql}")
        # Execute update query
        cur.execute(sql)

        # Commit the changes to the database
        conn.commit()

        # Close the cursor and connection
        cur.close()
        conn.close()

        AddToLog("Written/updated!")
        return 1
    except Exception as err:
        AddToLog(traceback.format_exc())
        return 0
    return ok

def StoreFlow2DB(user_name, time_s, flow_json):

    conn = get_db_connection()
    cur = conn.cursor()

    #query = """
    #INSERT INTO public.node_reds (user_name, last_activity, flow)
    #VALUES (%s, %s, %s)
    #ON CONFLICT (user_name)
    #DO UPDATE SET
        #last_activity = EXCLUDED.last_activity,
        #flow = EXCLUDED.flow
    #"""

    query = f"""
    INSERT INTO public.node_reds (user_name, last_activity, flow)
    VALUES ('{user_name}', {time_s}, '{flow_json}')
    ON CONFLICT (user_name)
    DO UPDATE SET
        last_activity = EXCLUDED.last_activity,
        flow = EXCLUDED.flow
    """
    logger.debug(f"sql= {query}")

    try:
        #cur.execute(query, (user_name, time_s, flow_json))
        cur.execute(query)
        conn.commit()
        logger.debug(f"OK!")
        return True
    except Exception as e:
        conn.rollback()
        print(f"Error storing flow: {e}")
        logger.debug(f"Error storing flow: {e}")
        return False
    finally:
        cur.close()
        conn.close()
        logger.debug(f"Closing!")

def CleanObject(object_in, typee = "s"):

    if typee == "n":
        res = 0
        if object_in == None or object_in == "":
            return 0
        if isinstance(object_in, str):
            try:
                res = object_in.replace("'", '"')
            except:
                pass
            return res
    else:
        res = ""
        if object_in == None:
            return ""
        if isinstance(object_in, str):
            try:
                res = object_in.replace("'", '"')
            except:
                pass
            return res
    return object_in

def StoreDeployment2DB(parameters, editing_deployment_id):
    conn = get_db_connection()
    cur = conn.cursor()

    if editing_deployment_id == None or editing_deployment_id == "":
        editing_deployment_id = "0"

    try:

        current_utc_time = datetime.datetime.now(timezone.utc)

        # Convert to epoch time
        current_epoch_time = current_utc_time.timestamp()

        if editing_deployment_id != "0":
            sql = f"""
            UPDATE public.deployments
            SET
                persons = {CleanObject(parameters.get('persons'), "n")},
                gender = {CleanObject(parameters.get('gender'), "n")},
                race = {CleanObject(parameters.get('race'), "n")},
                born = {CleanObject(parameters.get('born'), "n")},
                pets = {CleanObject(parameters.get('pets'), "n")},
                time_zone_s = '{CleanObject(parameters.get('time_zone_s'))}',
                user_edit = {CleanObject(parameters.get('user_id'), "n")},
                time_edit = {current_epoch_time}
            WHERE deployment_id = {CleanObject(editing_deployment_id, "n")};
            """

        else:
            sql = f"""
            INSERT INTO public.deployments
            (persons, gender, race, born, pets, time_zone_s, user_edit, time_edit)
            VALUES
            ({CleanObject(parameters.get('persons'), "n")}, {CleanObject(parameters.get('gender'), "n")}, {CleanObject(parameters.get('race'), "n")},
            {CleanObject(parameters.get('born'), "n")}, {CleanObject(parameters.get('pets'), "n")}, '{CleanObject(parameters.get('time_zone_s'))}',
            {CleanObject(parameters.get('user_id'), "n")}, {current_epoch_time})
            RETURNING deployment_id;
            """
        logger.debug(f"sql= {sql}")
        # Execute update query
        cur.execute(sql)
        if editing_deployment_id == "0":
            new_deployment_id = cur.fetchone()[0]
        # Commit the changes to the database
        conn.commit()

        if editing_deployment_id != "0":
            sql = f"""
            UPDATE public.deployment_details
            SET
                beneficiary_id = {CleanObject(parameters.get('beneficiary_id'), "n")},
                caretaker_id = {CleanObject(parameters.get('caretaker_id'), "n")},
                owner_id = {CleanObject(parameters.get('owner_id'), "n")},
                installer_id = {CleanObject(parameters.get('installer_id'), "n")},
                address_street = '{CleanObject(parameters.get('address_street'))}',
                address_city = '{CleanObject(parameters.get('address_city'))}',
                address_zip = '{CleanObject(parameters.get('address_zip'))}',
                address_state = '{CleanObject(parameters.get('address_state'))}',
                address_country = '{CleanObject(parameters.get('address_country'))}',

                wifis = '{CleanObject(parameters.get('wifis'))}',
                devices = '{CleanObject(parameters.get('devices'))}',
                lat = {CleanObject(parameters.get('lat'), "n")},
                lng = {CleanObject(parameters.get('lng'), "n")},
                gps_age = {CleanObject(parameters.get('gps_age'), "n")}

            WHERE deployment_id = {editing_deployment_id};
            """

        else:
            sql = f"""
            INSERT INTO public.deployment_details
            (deployment_id, beneficiary_id, caretaker_id, owner_id, installer_id, address_street, address_city, address_zip, address_state, address_country)
            VALUES
            ({new_deployment_id}, {CleanObject(parameters.get('beneficiary_id'), "n")}, {CleanObject(parameters.get('caretaker_id'), "n")}, {CleanObject(parameters.get('owner_id'), "n")}, {CleanObject(parameters.get('installer_id'), "n")},
            '{CleanObject(parameters.get('address_street'))}', '{CleanObject(parameters.get('address_city'))}', '{CleanObject(parameters.get('address_zip'))}', '{CleanObject(parameters.get('address_state'))}',
            '{CleanObject(parameters.get('address_country'))}');
            """
        logger.debug(f"sql= {sql}")
        # Execute update query
        cur.execute(sql)

        # Commit the changes to the database
        conn.commit()




        # Close the cursor and connection
        cur.close()
        conn.close()

        AddToLog("Written/updated!")
        return 1
    except Exception as err:
        AddToLog(traceback.format_exc())
        return 0
    return ok

def StoreDevice2DB(parameters, editing_device_id):

    # Create a device object. This object has nested properties and various types including numbers, DateTimes and strings.
    # This can be saved as JSON as is without converting into rows/columns.
    conn = get_db_connection()
    cur = conn.cursor()

    if editing_device_id == None or editing_device_id == "":
        editing_device_id = "0"

    try:

        current_utc_time = datetime.datetime.now(timezone.utc)

        # Convert to epoch time
        current_epoch_time = current_utc_time.timestamp()

        if editing_device_id != "0":
            sql = f"""
            UPDATE public.devices
            SET
                device_mac = '{CleanObject(parameters.get('device_mac'))}',
                well_id = '{CleanObject(parameters.get('well_id'))}',
                description = '{CleanObject(parameters.get('description'))}',
                location = '{CleanObject(parameters.get('location'))}',
                close_to = '{CleanObject(parameters.get('close_to'))}',
                radar_threshold = '{CleanObject(parameters.get('radar_threshold'))}',
                temperature_calib = '{CleanObject(parameters.get('temperature_calib'))}',
                humidity_calib = '{CleanObject(parameters.get('humidity_calib'))}'
            WHERE device_id = {editing_device_id};
            """

        else:
            sql = f"""
            INSERT INTO public.devices
            (device_mac, well_id, description, location, close_to, radar_threshold, temperature_calib, humidity_calib)
            VALUES
            ('{CleanObject(parameters.get('device_mac'))}', '{CleanObject(parameters.get('well_id'))}', '{CleanObject(parameters.get('description'))}',
            '{CleanObject(parameters.get('location'))}', '{CleanObject(parameters.get('close_to'))}', '{CleanObject(parameters.get('radar_threshold'))}',
            '{CleanObject(parameters.get('temperature_calib'))}', '{CleanObject(parameters.get('humidity_calib'))}');
            """
        logger.debug(f"sql= {sql}")
        # Execute update query
        cur.execute(sql)

        # Commit the changes to the database
        conn.commit()

        # Close the cursor and connection
        cur.close()
        conn.close()

        AddToLog("Written/updated!")
        return 1
    except Exception as err:
        AddToLog(traceback.format_exc())
        return 0
    return ok


def ShowAge(seconds):
    minutes = int(seconds // 60)
    hours = int(minutes // 60)
    days = int(hours // 24)

    if seconds >= 0:
        hours = int(hours - (days * 24))
        minutes = int(minutes - (days * 24 * 60) - (hours * 60))
        seconds = int(seconds - (days * 24 * 60 * 60) - (hours * 60 * 60) - (minutes * 60))

        if days > 0:
            report = f"{int(days)} d {int(hours)} h {int(minutes)} m {int(seconds)} s"
        elif hours > 0:
            report = f"{int(hours)} h {int(minutes)} m {int(seconds)} s"
        elif minutes > 0:
            report = f"{int(minutes)} m {int(seconds)} s"
        else:
            report = f"{int(seconds)} s"

        return report
    else:
        return "0 s"

def UpdateDevicesTable(html_string, devices, users):
    #ID, Well id, MAC,	Last_Message,	Location,	Description,	Deployment
    table_rows_string = ""
    for device in devices:
        #result = next(item for item in users if item[0] == device[6])

        deployment_name = "?"
        for item in users:
            if item[0] == device[6]:
                result = item
                deployment_name = result[1]
                if result[2] != None:
                    deployment_name = deployment_name + " " + result[2]
                break




        mac = device[2]
        if mac == "64B7088903B4":
            print("stop")
        mac_row_string = f'      <td style="text-align:center"><a href="#" onclick="OpenDevice(\'{mac}\'); return false;">{mac}</a></td>\n'
        age = time.time() - device[3]

        if (age < 300):
            row_sting = f'    <tr style="background-color: #90FFD7;">\n      <th scope="row" style="text-align:center">{device[0]}</th>\n'
        else:
            row_sting = f'    <tr>\n      <th scope="row" style="text-align:center">{device[0]}</th>\n'

        row_ending = f'      <td style="text-align:center"><input type="checkbox" onchange="IsItSingle();"></td>\n    </tr>\n'
        for col_cnt in range(1, len(device)):
            column_value = device[col_cnt]
            #print(column_value)
            if col_cnt == 2:
                col_string_template = mac_row_string
            elif col_cnt == 3:
                if column_value > 0:
                    col_string_template = f'      <td style="text-align:center">{ShowAge(age)}</td>\n'
                else:
                    col_string_template = f'      <td style="text-align:center">No data</td>\n'
            elif col_cnt == 4:
                col_string_template = f'      <td style="text-align:center">{column_value}</td>\n'
            elif col_cnt == 5:
                if column_value == None:
                    col_string_template = f'      <td style="text-align:center"></td>\n'
                else:
                    col_string_template = f'      <td style="text-align:center">{column_value}</td>\n'
            elif col_cnt == 6:
                col_string_template = f'      <td style="text-align:center"><a href="#" onclick="OpenDeployment(\'{column_value}\')" title="{deployment_name}">{column_value}</a></td>\n';
            else:
                if column_value == None:
                    column_value = ""
                col_string_template = f'      <td style="text-align:center">{column_value}</td>\n'
            row_sting = row_sting + col_string_template
        row_sting = row_sting + row_ending
        table_rows_string = table_rows_string + row_sting
        #print(table_rows_string)

    html_string = html_string.replace("###ROWS###",table_rows_string)
    return html_string

def UpdateDeploymentsSelector(html_string, deployments, include_all=True, selected="1"):
    #                  <option value="All" selected>All</option>
    if include_all:
        selector_string = f'                  <option value="0">All</option>\n'
    else:
        selector_string = ''

    for deployment in deployments:
        first_name = ""
        last_name = ""
        if deployment[1] != None:
            first_name = deployment[1]
        if deployment[2] != None:
            last_name = deployment[2]
        if deployment[0] == int(selected):
            choice_string = f'                  <option value="{deployment[0]}" selected>{deployment[0]} {first_name} {last_name}</option>\n'
        else:
            choice_string = f'                  <option value="{deployment[0]}">{deployment[0]} {first_name} {last_name}</option>\n'
        selector_string =  selector_string + choice_string
    #print(selector_string)

    html_string = html_string.replace("###INSTALLS###",selector_string)
    return html_string

def GetDeviceDetails(cur, deployment_ids, location_id):

    #ID, Well id, MAC,	Last_Message,	Location,	Description,	Deployment
    macs = [mac for _, mac in deployment_ids]
    #macs = list(deployment_ids.keys())
    macs_string_nq = ",".join(macs)
    macs_string = "'" + "','".join(macs) + "'"

    if location_id == -1:
        sql = f"""
        WITH ordered_macs AS (
          SELECT unnest(string_to_array('{macs_string_nq}', ',')) as mac,
                 generate_series(1, array_length(string_to_array('{macs_string_nq}', ','), 1)) as position
        )
        SELECT d.*
        FROM public.devices d
        JOIN ordered_macs om ON d.device_mac = om.mac::text
        WHERE device_mac IN ({macs_string})
        ORDER BY om.position;
        """
    else:
        sql = f"""
        WITH ordered_macs AS (
          SELECT unnest(string_to_array('{macs_string_nq}', ',')) as mac,
                 generate_series(1, array_length(string_to_array('{macs_string_nq}', ','), 1)) as position
        )
        SELECT d.*
        FROM public.devices d
        JOIN ordered_macs om ON d.device_mac = om.mac::text
        WHERE device_mac IN ({macs_string}) AND location = {location_id}
        ORDER BY om.position;
        """

    cur.execute(sql)
    print(sql)
    devices_ids_records = cur.fetchall()
    all_details = []


    devices_ids_list = [x[0] for x in devices_ids_records]
    device_ids_string = ",".join(map(str, devices_ids_list))
    #sql = f"SELECT device_id, MAX(time) as last_reading_time FROM sensor_readings WHERE device_id IN ({device_ids_string}) GROUP BY device_id" #to slow
    sql = f"SELECT DISTINCT ON (device_id) device_id, time as last_reading_time FROM sensor_readings WHERE device_id IN ({device_ids_string}) AND time > now() - INTERVAL '1 day' ORDER BY device_id, time DESC"
    cur.execute(sql)
    print(sql)
    devices_times = cur.fetchall()#cur.fetchone()
    found_device_details = {}
    for device_record in devices_times:
        device_id, last_message_time = device_record
        found_device_details[device_id] = last_message_time
    cnt = 0
    for device_table_record in devices_ids_records:
        if len(devices_times) > 0:

            if device_id in found_device_details:
                last_message_time = found_device_details[device_id]
                last_message_epoch = int(last_message_time.timestamp())
            else:
                try:
                    last_message_time = int(device_table_record[14])
                except:
                    last_message_time = 0
                last_message_epoch = last_message_time
        else:
            last_message_time = 0
            last_message_epoch = 0

        #print(last_message_epoch)
        #print(type(last_message_epoch))
        device_id = device_table_record[0]
        mac = device_table_record[1]
        well_id = device_table_record[2]
        description = device_table_record[3]
        if description == None:
            description = ""
        if device_table_record[5] != None:
            if device_table_record[5] != "":
                description =  description + " Close to " + device_table_record[5]
        location_id = device_table_record[4]
        if  location_id == None:
            location_id = 0
        try:
            row_data = [device_id, well_id, mac, last_message_epoch, location_names[location_id], description, deployment_ids[cnt][0]]
        except:
            row_data = [device_id, well_id, mac, last_message_epoch, location_names[location_id], description, deployment_ids[cnt][0]]
        cnt += 1
        all_details.append(row_data)

    return all_details

def GetVisibleDevices(deployments):

    devices_details = []
    stt = time.time()
    with get_db_connection() as conn:
        with conn.cursor() as cur:
            #list all devices that user has access to
            if deployments == "-1":
                sql = "SELECT device_mac FROM public.devices ORDER BY device_id ASC"# SELECT deployment_id, devices FROM public.deployment_details"
                macs_group = []
                deployment_ids = []
                print(sql)
                cur.execute(sql)
                macs_records = cur.fetchall()#cur.fetchone()
                for record in macs_records:
                    deployment_ids.append((0, record[0]))
                devices_details = GetDeviceDetails(cur, deployment_ids, -1)
            else:
                sql = f"SELECT deployment_id, devices FROM public.deployment_details WHERE deployment_id IN ({deployments})"

                print(sql)
                cur.execute(sql)
                devices_groups = cur.fetchall()#cur.fetchone()
                deployment_ids = []
                for deployment_id, dev_group in devices_groups:
                    if dev_group != None:
                        if len(dev_group) > 10:
                            if "[" not in dev_group:
                                if "," not in dev_group:
                                    dev_group = '["' + dev_group + '"]'
                                else:
                                    dev_group = dev_group.replace(" ", "")
                                    dev_group = dev_group.replace(",", '","')
                                    dev_group = '["' + dev_group + '"]'

                            macs_group = literal_eval(dev_group)

                            for mac in macs_group:
                                deployment_ids.append((deployment_id, mac))
                    else:
                        print(f"Deployment {deployment_id} has dev_group empty")
                devices_details = GetDeviceDetails(cur, deployment_ids, -1)
                #devices_details.append(devices_detail)

    return devices_details

def GetVisibleDevicesPerLocation(deployments, location):

    devices_details = []

    with get_db_connection() as conn:
        #list all devices that user has access to
        if deployments == "-1" or deployments == "0":
            sql = "SELECT deployment_id, devices FROM public.deployment_details"
        else:
            sql = f"SELECT deployment_id, devices FROM public.deployment_details WHERE deployment_id IN ({deployments})"

        with conn.cursor() as cur:
            cur.execute(sql)
            devices_groups = cur.fetchall()#cur.fetchone()
            deployment_ids = []
            for deployment_id, dev_group in devices_groups:
                if dev_group != None:
                    if len(dev_group) > 10:
                        if dev_group[0] == "[":
                            macs_group = literal_eval(dev_group)
                        else:
                            macs_group =  dev_group.split(',')
                        for mac in macs_group:
                            deployment_ids.append((deployment_id, mac))

            devices_details = GetDeviceDetails(cur, deployment_ids, location_indexes[location])
            #devices_details.append(devices_detail)

    return devices_details

def GetUsersFromDeployments(deployments):
    #list all devices that user has access to
    deployments_dets = []
    with get_db_connection() as conn:
        try:
            if deployments == "-1":
                sql = f"""
                SELECT dd.deployment_id, pd.first_name, pd.last_name
                FROM deployment_details dd
                JOIN person_details pd ON dd.beneficiary_id = pd.user_id
                ORDER BY dd.deployment_id;
                """
            else:
                sql = f"""
                SELECT dd.deployment_id, pd.first_name, pd.last_name
                FROM deployment_details dd
                JOIN person_details pd ON dd.beneficiary_id = pd.user_id
                WHERE dd.deployment_id IN ({deployments})
                ORDER BY dd.deployment_id;
                """
            with conn.cursor() as cur:
                cur.execute(sql)
                deployments_dets = cur.fetchall()#cur.fetchone()
        except Exception as err:
            logger.error("GetUsersFromDeployments "+str(err) +"  "+sql)

    return deployments_dets

def GetPreviousDate(current_date):
    date_obj = datetime.datetime.strptime(current_date, "%Y-%m-%d")
    # Subtract one day
    previous_date = date_obj - timedelta(days=1)
    # Convert back to string format
    previous_date_str = previous_date.strftime("%Y-%m-%d")
    return(previous_date_str)

def CovertToIsoTime(date_s, n_minute):

    hours = n_minute // 60  # Integer division
    minutes = n_minute % 60

    base_date = datetime.datetime.strptime(date_s, "%Y-%m-%d")
    final_datetime = base_date + timedelta(hours=hours, minutes=minutes)
    iso_timestamp = final_datetime.isoformat()
    return iso_timestamp

def GetSensorsDetailsFromDeployment(deployment_id, ddate, filter_minutes):
    #list all devices that user has access to
    deployments_dets = []
    with get_db_connection() as conn:
        try:
            sql = f"""
             SELECT pd.user_id, pd.first_name, pd.last_name, pd.address_street, pd.picture
             FROM deployment_details dd
             JOIN person_details pd ON dd.beneficiary_id = pd.user_id
             WHERE dd.deployment_id ={deployment_id};
             """
            with conn.cursor() as cur:
                cur.execute(sql)
                deployments_dets = cur.fetchone()
        except Exception as err:
            logger.error("GetSensorsDetailsFromDeployment "+str(err) +"  "+sql)

        time_zone_s = GetTimeZoneOfDeployment(deployment_id)
        timee = LocalDateToUTCEpoch(ddate, time_zone_s)+5 #add so date boundary is avoided
        devices_list, device_ids =  GetProximityList(deployment_id, timee)

        #Which sensor is in: Bathroom, Kitchen, Bedroom ?
        bathrooms = []
        kitchens = []
        bedrooms = []
        dev_id_to_location = {0: "Outside/?"}
        for device in devices_list:
            dev_id_to_location[device[1]] = device[2]
            if Consolidataed_locations[device[2]] == "Bathroom":
                bathrooms.append(device[1])
            elif Consolidataed_locations[device[2]] == "Kitchen":
                kitchens.append(device[1])
            elif Consolidataed_locations[device[2]] == "Bedroom":
                bedrooms.append(device[1])

        #we need to determine where user is seen last, and user sensor data from there...
        locations_file = f"/{deployment_id}/{deployment_id}_{ddate}_{filter_minutes}_daily_locations.png"
        logger.debug(f"locations_file1 ={locations_file}")
        locations_list_s = ReadObjectMinIO("daily-maps", locations_file+".bin")

        force_recreate = False
        file_exists1, file_modified_utc1 = check_file_exists(locations_file+".bin")
        if file_exists1:
            file_modified_local = file_modified_utc1.astimezone(pytz.timezone(time_zone_s))
            file_modified_date_local = file_modified_local.date() #local date
            file_modified_date_utc = file_modified_utc1.date()
            file_date_utc = MapFileToDate(locations_file) #locations_file is UTC
            #if file_modified_date_local < file_date_utc:
            if file_modified_utc1.date() < file_date_utc:
                force_recreate = True
            else: #same date
                current_time = datetime.datetime.now(pytz.timezone(time_zone_s))
                time_passed = current_time - file_modified_local
                if time_passed.seconds > 30: #recreate if older than 5 minutes
                    force_recreate = True
        else:
            force_recreate = True


        logger.debug(f"force_recreate={str(force_recreate)}")

        if force_recreate:
            CreateLocationsStripe(locations_file, time_zone_s)
            locations_list_s = ReadObjectMinIO("daily-maps", locations_file+".bin")


        last_present_device = 0
        last_present = 0
        last_bathroom = 0
        last_kitchen = 0
        last_bedroom = 0
        last_bathroom_date = ddate
        last_kitchen_date = ddate
        last_bedroom_date = ddate

        if locations_list_s is not None:
            locations_list = json.loads(locations_list_s)

            if len(locations_list) > 1:
                if locations_list[-1][0] > 0:
                    last_present_device = locations_list[-1][0]
                    last_present = locations_list[-1][1] + locations_list[-1][2]
                else:
                    last_present_device = locations_list[-2][0]
                    last_present = locations_list[-2][1] + locations_list[-2][2]
            elif len(locations_list) ==  1:
                last_present_device = locations_list[0][0]


            #Lets find last bathroom presence time

            if len(locations_list) > 0 and len(bathrooms) > 0:
                for loc_time in reversed(locations_list):
                    for device_id_temp in bathrooms:
                        if device_id_temp == loc_time[0]:
                            if (loc_time[1] + loc_time[2]) > last_bathroom:
                                last_bathroom = loc_time[1] + loc_time[2]
                                last_bathroom_date = ddate


            #Lets find last kitchen presence time

            if len(locations_list) > 0 and len(kitchens) > 0:
                for loc_time in reversed(locations_list):
                    for device_id_temp in kitchens:
                        if device_id_temp == loc_time[0]:
                            if (loc_time[1] + loc_time[2]) > last_kitchen:
                                last_kitchen = loc_time[1] + loc_time[2]
                                last_kitchen_date = ddate


            #Lets find last bedroom presence time

            if len(locations_list) > 0 and len(bedrooms) > 0:
                for loc_time in reversed(locations_list):
                    for device_id_temp in bedrooms:
                        if device_id_temp == loc_time[0]:
                            if (loc_time[1] + loc_time[2]) > last_bedroom:
                                last_bedroom = loc_time[1] + loc_time[2]
                                last_bedroom_date = ddate


        if last_bathroom == 0 or last_kitchen == 0 or last_bedroom == 0:
            ddate = GetPreviousDate(ddate)

            locations_file = f"/{deployment_id}/{deployment_id}_{ddate}_{filter_minutes}_daily_locations.png"
            logger.debug(f"locations_file2 ={locations_file}")

            force_recreate = False
            file_exists1, file_modified_utc1 = check_file_exists(locations_file+".bin")
            logger.debug(f"file_exists1={str(file_exists1)}")
            logger.debug(f"file_modified_utc1={str(file_modified_utc1)}")
            #file_exists1, file_modified_utc1
            if file_exists1:
                file_modified_local = file_modified_utc1.astimezone(pytz.timezone(time_zone_s))
                file_modified_date_local = file_modified_local.date()
                file_date_utc = MapFileToDate(locations_file)
                logger.debug(f"file_modified_utc1={str(file_modified_utc1.date())} file_date_utc={str(file_date_utc)}")
                if file_modified_utc1.date() < file_date_utc:
                    force_recreate = True
                else: #same date
                    current_time = datetime.datetime.now(pytz.timezone(time_zone_s))
                    time_passed = current_time - file_modified_local
                    logger.debug(f"current_time={current_time} file_modified_local={file_modified_local} time_passed={time_passed}")
                    if time_passed.seconds > 30: #recreate if older than 5 minutes
                        force_recreate = True
            else:
                force_recreate = True


            if force_recreate:
                CreateLocationsStripe(locations_file, time_zone_s)
            locations_list_s = ReadObjectMinIO("daily-maps", locations_file+".bin")



            logger.debug(f"locations_list_s={locations_list_s}")

            if (locations_list_s is not None):

                locations_list = json.loads(locations_list_s)

                if last_present_device == 0:
                    if len(locations_list) > 1:
                        if locations_list[-1][0] > 0:
                            last_present_device = locations_list[-1][0]
                        else:
                            last_present_device = locations_list[-2][0]
                    elif len(locations_list) ==  1:
                        last_present_device = locations_list[0][0]

                if last_bathroom == 0:
                    if len(locations_list) > 0 and len(bathrooms) > 0:
                        for loc_time in reversed(locations_list):
                            for device_id_temp in bathrooms:
                                if device_id_temp == loc_time[0]:
                                    if (loc_time[1] + loc_time[2]) > last_bathroom:
                                        last_bathroom = loc_time[1] + loc_time[2]
                                        last_bathroom_date = ddate

                if last_kitchen == 0:
                    if len(locations_list) > 0 and len(kitchens) > 0:
                        for loc_time in reversed(locations_list):
                            for device_id_temp in kitchens:
                                if device_id_temp == loc_time[0]:
                                    if (loc_time[1] + loc_time[2]) > last_kitchen:
                                        last_kitchen = loc_time[1] + loc_time[2]
                                        last_kitchen_date = ddate

                if last_bedroom == 0:
                    if len(locations_list) > 0 and len(bedrooms) > 0:
                        for loc_time in reversed(locations_list):
                            for device_id_temp in bedrooms:
                                if device_id_temp == loc_time[0]:
                                    if (loc_time[1] + loc_time[2]) > last_bedroom:
                                        last_bedroom = loc_time[1] + loc_time[2]
                                        last_bedroom_date = ddate



        last_bathroom_time = "2023-01-01T00:00:00"
        if last_bathroom > 0:
            last_bathroom_time = CovertToIsoTime(last_bathroom_date, last_bathroom)

        last_kitchen_time = "2023-01-01T00:00:00"
        if last_kitchen > 0:
            last_kitchen_time = CovertToIsoTime(last_kitchen_date, last_kitchen)

        last_bedroom_time = "2023-01-01T00:00:00"
        if last_bedroom > 0:
            last_bedroom_time = CovertToIsoTime(last_bedroom_date, last_bedroom)

        last_present_time = "2023-01-01T00:00:00"
        if last_present > 0:
            last_present_time = CovertToIsoTime(ddate, last_present)

        # debug for 48h bug
        if last_bathroom_time == "2023-01-01T00:00:00" or last_kitchen_time == "2023-01-01T00:00:00" or last_bedroom_time == "2023-01-01T00:00:00":
            #last_bathroom_time = "48h" if last_bathroom_time == "2023-01-01T00:00:00" else f"{last_bathroom-last_bathroom_time}"
            #last_kitchen_time = "48h" if last_kitchen_time == "2023-01-01T00:00:00" else f"{last_kitchen-last_kitchen_time}"
            #last_bedroom_time = "48h" if last_bedroom_time == "2023-01-01T00:00:00" else f"{last_bedroom-last_bedroom_time}"

            logger.debug(f"48h-> deployment_id={str(deployment_id)}, ddate={str(ddate)}")
            logger.debug(f"48h-> force_recreate={force_recreate}")
            logger.debug(f"48h-> last_bathroom_time={last_bathroom_time}|last_kitchen_time={last_kitchen_time}|last_bedroom_time={last_bedroom_time}")
            logger.debug(f"48h-> devices_list={str(devices_list)}")
            logger.debug(f"48h-> bathrooms={str(bathrooms)}")
            logger.debug(f"48h-> kitchens={str(kitchens)}")
            logger.debug(f"48h-> bedrooms={str(bedrooms)}")
            logger.debug(f"48h-> locations_list_s={str(locations_list_s)}")

        try:
            sql = f"""
             SELECT * FROM sensor_readings
             WHERE device_id = {last_present_device}
             ORDER BY time DESC
             LIMIT 1;
             """
            with conn.cursor() as cur:
                cur.execute(sql)
                sensor_dets = cur.fetchone()
        except Exception as err:
            logger.error("GetSensorsDetailsFromDeployment1 "+str(err) +"  "+sql)

        # wellness_score_percent
        wellness_score_percent = 90

        # bedroom_temperature
        bedroom_temperature = 0

        # sleep_bathroom_visit_count
        sleep_bathroom_visit_count = 0

        # bedroom_co2
        bedroom_co2 = 400

        # shower_detected_time
        shower_detected_time = last_bathroom_time

        # breakfast_detected_time
        breakfast_detected_time = 0

        # living_room_detected_time
        living_room_time_spent = 0

        # outside_hours
        outside_hours = 0

        #lets find last time seen at Bathroom, Kitchen, Bedroom  pd.first_name, pd.last_name, pd.address_street, pd.picture

        picture_url = deployments_dets[4]
        report = {}
        try:

            report = {"user_id":deployments_dets[0],
                      "name":deployments_dets[1] + " " + deployments_dets[2],
                      "address":deployments_dets[3],
                      "time_zone":time_zone_s,
                      "picture":picture_url,
                      "bathroom_at": last_bathroom_time,
                      "kitchen_at": last_kitchen_time,
                      "bedroom_at": last_bedroom_time,
                      "temperature": (sensor_dets[2] - 16) if sensor_dets != None else 0,
                      "smell": "clean",
                      "bathroom_delayed": [6, 12],
                      "kitchen_delayed": [6, 12],
                      "bedroom_delayed": [13, 16],
                      "last_location": dev_id_to_location[last_present_device],
                      "last_detected_time": last_present_time,
                      "wellness_score_percent": wellness_score_percent,
                      "wellness_descriptor_color": "bg-green-100 text-green-700",
                      "bedroom_temperature": bedroom_temperature,
                      "sleep_bathroom_visit_count": sleep_bathroom_visit_count,
                      "bedroom_co2": bedroom_co2,
                      "shower_detected_time": shower_detected_time,
                      "breakfast_detected_time": breakfast_detected_time,
                      "living_room_time_spent": living_room_time_spent,
                      "outside_hours": outside_hours,
                      "wellness_descriptor": "Great!",
                      "last_seen_alert": "Alert = None",
                      "last_seen_alert_colors": "bg-green-100 text-green-700", #https://tailwindcss.com/docs/colors
                      "most_time_spent_in": "Bedroom",
                      "sleep_hours": "7.9"
                      }
        except:
            pass
    return report

def ToList(input_data):
    # If input is already a list
    if isinstance(input_data, list):
        return [str(x).strip() for x in input_data]

    # If input is string
    if isinstance(input_data, str):
        # Remove outer brackets if present
        cleaned = input_data.strip('()')
        cleaned = cleaned.strip('[]')
        # Remove extra quotes
        cleaned = cleaned.replace('"', '').replace("'", '')
        # Split by comma and clean each element
        return [x.strip() for x in cleaned.split(',')]

    raise ValueError(f"Unsupported input type: {type(input_data)}")

def MACsToWellIds(cur, macs_list):

    device_ids = []
    device_list = []

    macs_string = ",".join(f"'{mac}'" for mac in macs_list)
    if  macs_string != "'None'":
        sqlr = f"SELECT well_id, device_mac, device_id, location, description, radar_threshold, close_to FROM public.devices WHERE device_mac IN ({macs_string})"

        #print (sqlr)
        macs_map = {}
        cur.execute(sqlr)
        proximitys_list = cur.fetchall()
        for well_id, mac, device_id, location, description, radar_threshold, close_to in proximitys_list:
            macs_map[mac] = (well_id, device_id, location_names[location], description, mac, radar_threshold, close_to)


        for mac in macs_list:
            device_ids.append(macs_map[mac][1])
            device_list.append(macs_map[mac])

    return device_ids, device_list

def MACsStrToDevIds(cur, macs):

    device_ids = []
    #we need to repcakage string to contain '
    macs_list = ToList(macs)
    macs_string = ",".join(f"'{mac}'" for mac in macs_list)
    if  macs_string != "'None'":

        sqlr = f"SELECT device_mac, device_id FROM public.devices WHERE device_mac IN ({macs_string})"
        print (sqlr)
        macs_map = {}
        cur.execute(sqlr)
        proximitys_list = cur.fetchall()
        for mac, device_id in proximitys_list:
            device_ids.append((mac, device_id))

    return device_ids



def ReadCleanStringDB(cur, sql):
    cur.execute(sql)
    temp_string = cur.fetchone()
    if temp_string == None:
        return ""
    else:
        return str(temp_string[0]).strip()

# obtain device_list, device_ids for deployment_id on time as epoch_from_file_s (usually today)
# it tries first overridden/newly-installed (FROM public.deployment_history)
# then if none found there searches FROM public.deployment_details
def GetProximityList(deployment_id, epoch_from_file_s):

    #both are valid:
    #64B70888FA84,64B70888F6F0,64B70888F860,64B70889062C,64B70888FAB0,64B708896BDC,64B708897428
    #['64B70888FA84', '64B70888F6F0', '64B70888F860', '64B70889062C', '64B70888FAB0', '64B708896BDC', '64B708897428']

    #result_list = []
    #well_ids = []
    with get_db_connection() as conn:

        sqlr = f"""
            SELECT * FROM (
                SELECT proximity
                FROM public.deployment_history
                WHERE deployment_id = {deployment_id}
                AND time <= {epoch_from_file_s}
                ORDER BY time DESC
                LIMIT 1
            ) AS latest_deployment
        """
        #print (sqlr)
        with conn.cursor() as cur:
            devices_string = ReadCleanStringDB(cur, sqlr)

            if devices_string == "":
                sqlr = f"SELECT devices from public.deployment_details WHERE deployment_id ={deployment_id}"
                #print (sqlr)
                devices_string = ReadCleanStringDB(cur, sqlr)


            if devices_string == "":
                return []

            macs_list = ToList(devices_string)
            device_ids, device_list = MACsToWellIds(cur, macs_list)
    return device_list, device_ids

def FilterList(to_filter: str, allowed: str) -> str:
    # Convert comma-separated strings to sets
    filter_set = set(to_filter.split(','))
    allowed_set = set(allowed.split(','))

    # Find intersection and sort the result
    filtered = sorted(filter_set.intersection(allowed_set), key=int)

    # Join back to comma-separated string
    return ','.join(filtered)


def GetMatchingDevices(privileges, group, deployment, location):

    global LocationsMap

    results=[]
    if privileges != "-1":
        if deployment == "" or deployment == "0":
            deployment = privileges

        privileges_list = privileges.split(',')
        if deployment != "0":
            if "," in  deployment:
                deployment = FilterList(deployment, privileges)
            else:
                if deployment not in privileges_list:
                    return results
    else:
        if deployment == "0":
            deployment = "-1"

    devices = GetVisibleDevicesPerLocation(deployment, location)
    return devices

def getOldestDeploymentHistoryFromBeneficiary(deployment_id):
    #this will return oldest entry as well as last proximity (devices)
    st = time.time()
    print(f"*0 ----{time.time() - st}")
    results=[]
    well_ids_last = [] #this needs to be list of tuples (well_id, Location_st, Description)
    oldest_time = None
    try:
        print(f"*0a ----{time.time() - st}")
        with get_db_connection() as conn:
            sqlr = f"""
                SELECT * FROM (
                    SELECT time, proximity
                    FROM public.deployment_history
                    WHERE deployment_id = {deployment_id}
                    ORDER BY time ASC
                ) AS latest_deployment
            """
            print (sqlr)
            print(f"*1 ----{time.time() - st}")
            with conn.cursor() as cur:
                cur.execute(sqlr)
                print(f"*2 ----{time.time() - st}")
                results = cur.fetchall()
                print(f"*3 ----{time.time() - st}")
                #lets find which of historical sets has data in DB
                if results == None or results == []: #look in deployment_details
                    sqlr = f"SELECT devices from public.deployment_details WHERE deployment_id ={deployment_id}"
                    #print (sqlr)
                    print(f"*4 ----{time.time() - st}")
                    devices_string = ReadCleanStringDB(cur, sqlr)
                    print(f"*5 ----{time.time() - st}")
                    macs_list = ToList(devices_string)
                    print(f"*6 ----{time.time() - st}")
                    device_ids_last, device_alls_last = MACsToWellIds(cur, macs_list)
                    sql_query = """
                    SELECT device_id, first_seen_at
                    FROM device_first_seen
                    WHERE device_id = ANY(%s)
                    GROUP BY device_id;
                    """
                    print(f"*7 ----{time.time() - st}")
                    try:
                        cur.execute(sql_query, (device_ids_last,))
                        results1 = cur.fetchall()
                        if results1 == []:
                            pass
                        else:
                            oldest_time = results1[0][1]
                    except Exception as e:
                        AddToLog(traceback.format_exc())
                        AddToLog(str(e))
                    print(f"*8 ----{time.time() - st}")
                else:
                    history_entry = results[-1]
                    macs_list = ToList(history_entry[1])
                    print(f"*9 ----{time.time() - st}")
                    device_ids_last, device_alls_last = MACsToWellIds(cur, macs_list)


                    for history_entry in results:
                        macs_list = ToList(history_entry[1])
                        print(f"*10 ----{time.time() - st}")
                        device_ids, device_alls = MACsToWellIds(cur, macs_list)
                        print(f"*11 ----{time.time() - st}")
                        sql_query = """
                        SELECT time as oldest_record_time
                        FROM sensor_readings
                        WHERE device_id = ANY(%s)
                        ORDER BY time ASC
                        LIMIT 1;
                        """
                        print(f"*12 ----{time.time() - st}")
                        try:
                            cur.execute(sql_query, (device_ids_last,))
                            results1 = cur.fetchall()
                            oldest_time = results1[0][0]
                            if oldest_time != None:
                                break


                        except Exception as e:
                            print(str(e))
                        print(f"*13 ----{time.time() - st}")

    except Exception as e:
        print(f"*0b ----{time.time() - st}")
        AddToLog(traceback.format_exc())

    print(f"*14 ----{time.time() - st}")
    return oldest_time, device_alls_last


def getLastEditedBeneficiary(beneficiary):

    #lets generate token here to elliminate issues with outdated token...
    token = generate_token(beneficiary)
    url = 'https://well-api.azurewebsites.net/api/well_api'
    params = {
      "name": "beneficiary_detail",
      "beneficiary": beneficiary,
      "token": token
  }
    #{"id": "user_beneficiary_bernhard@wellnuo.com", "MAC": "BENEFICIARY", "email": "bernhard@wellnuo.com", "edit_date": "Fri Aug 16 06:45:01 2024", "c_password": "bern1", "first_name": "Bernhard", "last_name": "Knigge", "address": "776 Dubanski Dr.", "address_city": "San Jose", "address_state": "CA", "address_zip": "95123", "address_country": "United States", "phone_number": "4087055709", "persons": "2", "gender": "M", "race": "W", "born": "1972", "pets": "1", "creds": "", "devs": "[[203, 'Living Room', '', '64B708890B14'], [251, 'Bathroom', '', '64B7088909E8'], [252, 'Bedroom', '', '64B708890734'], [204, 'Bathroom', 'Guest', '64B708890288'], [201, 'Kitchen', 'toaster', '64B708890584'], [202, 'Kitchen', 'stove', '64B7088906D8'], [205, 'Office', '', '64B708897018']]", "tzone": "America/Los_Angeles", "ttl": -1, "_rid": "R60hANIG-K+qTQIAAAAAAg==", "_self": "dbs/R60hAA==/colls/R60hANIG-K8=/docs/R60hANIG-K+qTQIAAAAAAg==/", "_etag": "\"3500a0ae-0000-0800-0000-66bef56d0000\"", "_attachments": "attachments/", "_ts": 1723790701}
    response = requests.get(url, params=params)
    if response.status_code == 200:

        text = response.text
        #print(text)
        if text == "Log-Out":
            return text
        if text[0] == "{":
            data = json.loads(response.text)
            date_string = data["edit_date"]
            parsed_date = datetime.datetime.strptime(date_string, '%c')
            # Convert the datetime object to a timestamp (epoch time)
            epoch_str = str(time.mktime(parsed_date.timetuple()))
            devices = data["devs"]
            return(epoch_str, devices)
        else:
            return text,""
    else:
        logger.debug((f"Failed to retrieve the data, status code: {response.status_code}"))

    return "",""

def GetDeploymentNameFromId(Id):

    con = sqlite3.connect(main_db)
    con.text_factory = str
    cur = con.cursor()
    results=[]
    SQL = "SELECT name FROM deployments WHERE id =" + Id
    df = cur.execute(SQL)
    results = cur.fetchall()
    if len(results) > 0:
        return results[0][0]
    else:
        return ""

def GetTimeZoneOfDeployment(deployment_id):
    time_zone_st = 'America/Los_Angeles'
    with get_db_connection() as conn:
        with conn.cursor() as cur:
            sqlr = f"SELECT time_zone_s from public.deployments WHERE deployment_id ={deployment_id}"
            time_zone_st = ReadCleanStringDB(cur, sqlr)
    return time_zone_st

def StringToEpoch(date_string, time_zone_s):
    """
    Convert a date string to epoch timestamp for start of day (midnight) in specified timezone

    Args:
        date_string (str): Date in 'YYYY-MM-DD' format
        time_zone_s (str): Timezone string (e.g. 'America/Los_Angeles')

    Returns:
        float: Epoch timestamp in seconds
    """
    # Parse the date string
    date_format = '%Y-%m-%d'
    naive_date = datetime.datetime.strptime(date_string, date_format)

    # Get the timezone
    timezone = pytz.timezone(time_zone_s)

    # Localize the date to midnight in the specified timezone
    local_date = timezone.localize(naive_date)

    # Convert to epoch timestamp
    epoch_time = local_date.timestamp()

    return epoch_time

def LocalDateToUTCEpoch(local_date_str, time_zone_s):
    """
    Convert a date string to epoch timestamp for start of day (midnight) in UTC

    Args:
        local_date_str (str): Date in 'YYYY-MM-DD' format
        time_zone_s (str): Timezone string (e.g. 'America/Los_Angeles')

    Returns:
        float: Epoch UTC timestamp in seconds
    """
    timezone = pytz.timezone(time_zone_s)
    # Parse the date string
    date_format = '%Y-%m-%d'
    local_datetime = datetime.datetime.strptime(local_date_str, date_format)
    local_datetime = timezone.localize(local_datetime)

    utc_datetime = local_datetime.astimezone(pytz.UTC)
    epoch_time = int(utc_datetime.timestamp())

    return epoch_time

def GetDeploymentDatesBoth(deployment_in):

    #when looking at the date, date is defined in TZ where device is!
    #Lets take oldest data from first member of deployment
    st = time.time()
    date_list = []
    print(f"&0 ----{time.time() - st}")
    time_zone_st = GetTimeZoneOfDeployment(deployment_in)
    print(f"&1 ----{time.time() - st}")
    oldest_date_dt_utc, devices_all = getOldestDeploymentHistoryFromBeneficiary(deployment_in)
    print(f"&2 ----{time.time() - st}")
    if oldest_date_dt_utc != None:
        #get date in local time zone from UTC datetime

        #oldest_date_dt
        # Get today's date
        local_timezone = pytz.timezone(time_zone_st)  # Replace with your local timezone
        oldest_date_dt_local = oldest_date_dt_utc.astimezone(local_timezone)
        today_date = datetime.datetime.now(local_timezone)

        # Generate a list of date strings from oldest_date to today in inverted order
        date_list = [(today_date - timedelta(days=x)).strftime('%Y-%m-%d') for x in range((today_date - oldest_date_dt_local).days + 1)]
    print(f"&3 ----{time.time() - st}")
    return date_list, devices_all

def check_file_exists(file_name, bucket_name="daily-maps"):
    try:
        # Try to get the object's stats - this will raise an exception if the object doesn't exist
        stat_result = miniIO_blob_client.stat_object(bucket_name, file_name)
        last_modified_utc = stat_result.last_modified
        return True, last_modified_utc
    except S3Error as e:
        if e.code == 'NoSuchKey':
            return False, 0
        # Re-raise if it's a different error
        raise

def get_text_dimensions(text, font, font_scale, thickness):
    (width, height), baseline = cv2.getTextSize(text, font, font_scale, thickness)
    return {
        'width': width,
        'height': height,
        'baseline': baseline,
        'total_height': height + baseline
    }

def save_to_minio(image, filename, bucket_name="daily-maps", content_type="image/png"):
    """
    Save a PIL Image directly to MinIO

    Args:
        image (PIL.Image): Image to save
        filename (str): Filename to use in MinIO
        bucket_name (str): MinIO bucket name
        content_type (str): Content type of the file

    Returns:
        bool: True if successful, False otherwise
    """
    logger = logging.getLogger(__name__)
    try:
        # Convert PIL image to bytes
        img_byte_arr = io.BytesIO()
        image.save(img_byte_arr, format='PNG')
        img_byte_arr.seek(0)  # Move to start of the BytesIO buffer

        # Upload to MinIO
        miniIO_blob_client.put_object(
            DAILY_MAPS_BUCKET_NAME,
            filename,
            img_byte_arr,
            length=len(img_byte_arr.getvalue()),
            content_type=content_type
        )
        return True

    except Exception as e:
        logger.error(f"Error saving to MinIO: {traceback.format_exc()}")
        return False


def SaveImageInBlob(file_name, arr_stretched, labels = []):

    #labels=[(caption,(x,y),font,scale,color,thickness,line_type)]
    try:

        image_with_text = arr_stretched.copy()

        for label in labels:
            cv2.putText(
                image_with_text,      # Image
                label[0],             # Text to write
                label[1],             # Position (x, y)
                label[2],             # Font type
                label[3],             # Font scale
                label[4],             # Color (BGR)
                label[5],             # Thickness
                label[6]              # Line type
             )

        # Encode the image to a memory buffer using imencode
        success, encoded_image = cv2.imencode('.png', image_with_text)
        AddToLog(f"success={success}")
        if not success:
            raise Exception("Could not encode image!")

        #AddToLog(f"DAILY_MAPS_BUCKET_NAME={DAILY_MAPS_BUCKET_NAME}")

        image_bytes = encoded_image.tobytes()
        AddToLog(f"len(image_bytes)={len(image_bytes)}")
        miniIO_blob_client.put_object(
            DAILY_MAPS_BUCKET_NAME,
            file_name,
            io.BytesIO(image_bytes),
            len(image_bytes))
        return True
    except Exception as e:
        AddToLog(f"{traceback.format_exc()}")
        logger.error(f"{traceback.format_exc()}")
        return False

def SaveImageInBlobLabelsOut(file_name, arr_stretched, labels, title_labels):

    #labels=[(caption,(x,y),font,scale,color,thickness,line_type)]
    try:

        image_with_text = arr_stretched.copy()

        for label in labels:
            cv2.putText(
                image_with_text,      # Image
                label[0],             # Text to write
                label[1],             # Position (x, y)
                label[2],             # Font type
                label[3],             # Font scale
                label[4],             # Color (BGR)
                label[5],             # Thickness
                label[6]              # Line type
             )

        for label in title_labels:
            cv2.putText(
                image_with_text,      # Image
                label[0],             # Text to write
                label[1],             # Position (x, y)
                label[2],             # Font type
                label[3],             # Font scale
                label[4],             # Color (BGR)
                label[5],             # Thickness
                label[6]              # Line type
             )


        # Encode the image to a memory buffer using imencode
        success, encoded_image = cv2.imencode('.png', image_with_text)
        AddToLog(f"success={success}")
        if not success:
            raise Exception("Could not encode image!")

        #AddToLog(f"DAILY_MAPS_BUCKET_NAME={DAILY_MAPS_BUCKET_NAME}")

        image_bytes = encoded_image.tobytes()
        AddToLog(f"len(image_bytes)={len(image_bytes)}")
        miniIO_blob_client.put_object(
            DAILY_MAPS_BUCKET_NAME,
            file_name,
            io.BytesIO(image_bytes),
            len(image_bytes))
        return True
    except Exception as e:
        AddToLog(f"{traceback.format_exc()}")
        logger.error(f"{traceback.format_exc()}")
        return False

def GetLocalTimeForDate(selected_date, time_zone_s, minutes_padding = 0):
    # Parse the selected date
    local_tz = pytz.timezone(time_zone_s)

    # Convert selected_date string to datetime object (start of day in local time)
    local_date = datetime.datetime.strptime(selected_date, "%Y-%m-%d")
    local_start = local_tz.localize(local_date)

    # Get the next day
    local_next = local_start + timedelta(days=1)

    if minutes_padding > 0:
        local_start = local_start - timedelta(minutes=minutes_padding)
        local_next = local_next + timedelta(minutes=minutes_padding)

    # Convert to UTC
    utc_start = local_start.astimezone(pytz.UTC)
    utc_next = local_next.astimezone(pytz.UTC)

    # Format as strings
    time_from_str = utc_start.strftime("%Y-%m-%d %H:%M:%S")
    time_to_str = utc_next.strftime("%Y-%m-%d %H:%M:%S")

    return time_from_str + "+0000", time_to_str + "+0000"

def GetLocalTimeForDateSimple(selected_date, time_zone_s, minutes_padding = 0):
    # Parse the selected date
    local_tz = pytz.timezone(time_zone_s)

    # Convert selected_date string to datetime object (start of day in local time)
    local_date = datetime.datetime.strptime(selected_date, "%Y-%m-%d")
    local_start = local_tz.localize(local_date)

    # Get the next day
    local_next = local_start + timedelta(days=1)

    if minutes_padding > 0:
        local_start = local_start - timedelta(minutes=minutes_padding)
        local_next = local_next + timedelta(minutes=minutes_padding)

    # Convert to UTC
    utc_start = local_start.astimezone(pytz.UTC)
    utc_next = local_next.astimezone(pytz.UTC)

    return utc_start, utc_next

def GetLocalTimeEpochsForDate(selected_date, time_zone_s):
    """
    Get start and end of day epochs for a given date in a specific timezone.

    Args:
        selected_date (str): Date in "YYYY-MM-DD" format
        time_zone_s (str): Timezone string (e.g., "America/New_York")

    Returns:
        tuple: (start_epoch, end_epoch) - Unix timestamps for start and end of day
    """
    # Parse the selected date
    local_tz = pytz.timezone(time_zone_s)

    # Convert selected_date string to datetime object (start of day in local time)
    local_date = datetime.datetime.strptime(selected_date, "%Y-%m-%d")
    local_start = local_tz.localize(local_date)

    # Get the next day
    local_next = local_start + timedelta(days=1)

    # Convert to UTC
    utc_start = local_start.astimezone(pytz.UTC)
    utc_next = local_next.astimezone(pytz.UTC)

    # Convert to epochs (Unix timestamps)
    start_epoch = int(utc_start.timestamp())
    end_epoch = int(utc_next.timestamp())

    return start_epoch, end_epoch

def UTC2Local(utc_time, time_zone_s):
    # Parse the selected date
    local_tz = pytz.timezone(time_zone_s)

    # Convert selected_date string to datetime object (start of day in local time)
    #local_date = datetime.datetime.strptime(selected_date, "%Y-%m-%d")
    local_start = local_tz.localize(selected_date)

    # Convert to UTC
    utc_start = local_start.astimezone(pytz.UTC)
    utc_next = local_next.astimezone(pytz.UTC)

    # Format as strings
    time_from_str = utc_start.strftime("%Y-%m-%d %H:%M:%S")
    time_to_str = utc_next.strftime("%Y-%m-%d %H:%M:%S")

    return time_from_str + "+0000", time_to_str + "+0000"

def get_timezone_aware_datetime(time_str, timezone_str="America/Los_Angeles"):
    """
    Convert a naive datetime string to a timezone-aware datetime object.

    Parameters:
    time_str: String in format 'YYYY-MM-DD HH:MM:SS'
    timezone_str: String representing the timezone (default: "America/Los_Angeles")

    Returns:
    datetime: A timezone-aware datetime object
    """
    # Parse the naive datetime
    naive_dt = datetime.datetime.strptime(time_str, '%Y-%m-%d %H:%M:%S')

    # Get the timezone
    tz = pytz.timezone(timezone_str)

    # Localize the datetime (make it timezone-aware)
    # localize() is the correct way to do this, as it handles DST transitions properly
    aware_dt = tz.localize(naive_dt)
    return aware_dt

def fast_fill_array_from_timescale(day_data, time_from_str, devices_list, arr_source, timezone_str="Europe/Berlin"):
    """
    Optimized version of array filling from TimeScaleDB data.
    Uses vectorized operations for significant speed improvement.
    """
    # Convert start time to timezone-aware datetime
    start_time = datetime.datetime.strptime(time_from_str, '%Y-%m-%d %H:%M:%S%z')
    #start_time = start_time.replace(tzinfo=timezone.utc)

    # Create device index mapping
    device_to_index = {device_id: idx for idx, device_id in enumerate(devices_list)}

    # Define column mappings (sensor type to position in record)
    columns = {
        'avg_temperature': 2,
        'avg_humidity': 3,
        'pressure_amplitude': 4,
        'max_light': 5,
        'radar': 6
    }
    # Add sensor columns dynamically
    cols_len = len(columns)
    for i in range(10):
        columns[f'sensor_min_s{i}'] = i + cols_len #smell * 10 + 5

    # Pre-process data into a more efficient structure
    # Group by device_id to reduce lookup operations
    device_data = defaultdict(list)
    for record in day_data:
        if record[0] and record[1]:  # If time and device_id exist
            device_data[record[1]].append(record)

    # Process each device's data in bulk
    for device_id, records in device_data.items():
        if device_id not in device_to_index:
            continue

        base_idx = device_to_index[device_id] * len(columns)

        # Convert records to numpy array for faster processing
        records_array = np.array(records, dtype=object)

        # Calculate all minute deltas at once
        times = records_array[:, 0]
        #print(times[0], start_time, (times[0] - start_time).total_seconds())
        minute_deltas = np.array([(t - start_time).total_seconds() / 60 for t in times], dtype=int)

        # Filter valid minute deltas
        valid_mask = (minute_deltas >= 0) & (minute_deltas < arr_source.shape[1])
        if not np.any(valid_mask):
            continue

        minute_deltas = minute_deltas[valid_mask]
        records_array = records_array[valid_mask]

        # Process each column type in bulk
        for col_name, col_offset in columns.items():
            row_idx = base_idx + list(columns.keys()).index(col_name)
            values = records_array[:, col_offset]

            # Filter out None values
            valid_values = ~np.equal(values, None)
            if not np.any(valid_values):
                continue

            # Update array in bulk
            arr_source[row_idx, minute_deltas[valid_values]] = values[valid_values]

    return arr_source

def fast_fill_smell_array_from_timescale(day_data, time_from_str, device_to_index, arr_source, timezone_str="Europe/Berlin"):
    """
    Optimized version of array filling from TimeScaleDB data.
    Uses vectorized operations for significant speed improvement.
    """
    # Convert start time to timezone-aware datetime
    start_time = datetime.datetime.strptime(time_from_str, '%Y-%m-%d %H:%M:%S%z')
    #start_time = start_time.replace(tzinfo=timezone.utc)

    # Define column mappings (sensor type to position in record)
    columns = {
        's0': 2,
        's1': 3,
        's2': 4,
        's3': 5,
        's4': 6,
        's5': 7,
        's6': 8,
        's7': 9,
        's8': 10,
        's9': 11
    }
    ## Add sensor columns dynamically
    #cols_len = len(columns)
    #for i in range(10):
        #columns[f'sensor_min_s{i}'] = i + cols_len #smell * 10 + 5

    # Pre-process data into a more efficient structure
    # Group by device_id to reduce lookup operations
    device_data = defaultdict(list)
    for record in day_data:
        if record[0] and record[1]:  # If time and device_id exist
            device_data[record[1]].append(record)

    # Process each device's data in bulk
    for device_id, records in device_data.items():
        if device_id not in device_to_index:
            continue

        base_idx = device_to_index[device_id] * len(columns)

        # Convert records to numpy array for faster processing
        records_array = np.array(records, dtype=object)

        # Calculate all minute deltas at once
        times = records_array[:, 0]
        #print(times[0], start_time, (times[0] - start_time).total_seconds())
        minute_deltas = np.array([(t - start_time).total_seconds() / 60 for t in times], dtype=int)

        # Filter valid minute deltas
        valid_mask = (minute_deltas >= 0) & (minute_deltas < arr_source.shape[1])
        if not np.any(valid_mask):
            continue

        minute_deltas = minute_deltas[valid_mask]
        records_array = records_array[valid_mask]

        # Process each column type in bulk
        for col_name, col_offset in columns.items():
            row_idx = base_idx + list(columns.keys()).index(col_name)
            values = records_array[:, col_offset]

            # Filter out None values
            valid_values = ~np.equal(values, None)
            if not np.any(valid_values):
                continue

            # Update array in bulk
            arr_source[row_idx, minute_deltas[valid_values]] = values[valid_values]

    return arr_source

def fast_fill_radar_array_from_timescale(day_data, time_from_str, devices_list, arr_source, timezone_str="Europe/Berlin"):
    """
    Optimized version of array filling from TimeScaleDB data.
    Uses vectorized operations for significant speed improvement.
    """
    # Convert start time to timezone-aware datetime
    start_time = datetime.datetime.strptime(time_from_str, '%Y-%m-%d %H:%M:%S%z')
    #start_time = start_time.replace(tzinfo=timezone.utc)

    # Create device index mapping
    device_to_index = {device_id: idx for idx, device_id in enumerate(devices_list)}

    # Define column mappings (sensor type to position in record)
    columns = {
        's2': 2,
        's3': 3,
        's4': 4,
        's5': 5,
        's6': 6,
        's7': 7,
        's8': 8
    }

    # Pre-process data into a more efficient structure
    # Group by device_id to reduce lookup operations
    device_data = defaultdict(list)
    for record in day_data:
        if record[0] and record[1]:  # If time and device_id exist
            device_data[record[1]].append(record)

    # Process each device's data in bulk
    for device_id, records in device_data.items():
        if device_id not in device_to_index:
            continue

        base_idx = device_to_index[device_id] * len(columns)

        # Convert records to numpy array for faster processing
        records_array = np.array(records, dtype=object)

        # Calculate all minute deltas at once
        times = records_array[:, 0]
        #print(times[0], start_time, (times[0] - start_time).total_seconds())
        minute_deltas = np.array([(t - start_time).total_seconds() / 60 for t in times], dtype=int)

        # Filter valid minute deltas
        valid_mask = (minute_deltas >= 0) & (minute_deltas < arr_source.shape[1])
        if not np.any(valid_mask):
            continue

        minute_deltas = minute_deltas[valid_mask]
        records_array = records_array[valid_mask]

        # Process each column type in bulk
        for col_name, col_offset in columns.items():
            row_idx = base_idx + list(columns.keys()).index(col_name)
            values = records_array[:, col_offset]

            # Filter out None values
            valid_values = ~np.equal(values, None)
            if not np.any(valid_values):
                continue

            # Update array in bulk
            arr_source[row_idx, minute_deltas[valid_values]] = values[valid_values]

    return arr_source

def ScaleTemperature(temperature_in_f):
    # Define our key temperature points and their corresponding color values
    temp_points = [30, 50, 70, 90, 110]
    color_values = [768, 640, 384, 128, 0]  # Color values in the rainbow scale

    # Clamp the temperature to our range
    clamped_temp = max(min(temperature_in_f, 110), 30)

    # Find which segment the temperature falls into
    for i in range(len(temp_points) - 1):
        if temp_points[i] <= clamped_temp <= temp_points[i + 1]:
            # Linear interpolation between the two nearest points
            t = (clamped_temp - temp_points[i]) / (temp_points[i + 1] - temp_points[i])
            color_value = int(color_values[i] + t * (color_values[i + 1] - color_values[i]))
            return color_value

    # Fallback (should never reach here due to clamping)
    return 0

def GetTemperatureColor(temperature_in_f):
    color_value = ScaleTemperature(temperature_in_f)
    return BestColor(color_value)

def BestColor(in_val):
    #this function uses numbers from 0 to 1279 to convert to rainbow from Blue to Red(1024) to Violet 1279
    r,g,b=0,0,0
    in_val = int(in_val)
    if(in_val > 1279):
        in_val = 1279
    if (in_val < 256):
        r = 255
        g = in_val
    elif (in_val < 512):
        r = 511 - in_val
        g = 255
    elif (in_val < 768):
        g = 255
        b = in_val-512
    elif (in_val < 1024):
        g = 1023 - in_val
        b = 255
    else:
        r = in_val - 1024
        b = 255

    #if (r > 255):
    #    print(in_val)
    #    print(int(r),int(g),int(b))
    return(int(r),int(g),int(b))

def GrayColor(in_val):
    #this function uses numbers from 0 to 1279 to convert to rainbow from Blue to Red(1024) to Violet 1279
    r,g,b=0,0,0
    in_val = int(in_val)
    if(in_val < 0):
        in_val = 0
    if(in_val > 255):
        in_val = 255

    r = in_val
    g = r
    b = r
    return(int(r),int(g),int(b))

def fill_array_from_timescale(day_data, time_from_str, devices_list, arr_source, timezone_str):
    """
    Fill numpy array with data from TimeScaleDB query results.

    Parameters:
    day_data: List of tuples from database query
    time_from_str: Starting datetime string in format 'YYYY-MM-DD HH:MM:SS'
    devices_list: List of device IDs
    arr_source: Pre-initialized numpy array to fill

    Returns:
    numpy.ndarray: Filled array
    """
    # Parse the start time
    #start_time = get_timezone_aware_datetime(time_from_str, timezone_str)
    start_time = datetime.datetime.strptime(time_from_str, '%Y-%m-%d %H:%M:%S%z')
    # Create mapping of device_ids to their index positions
    device_to_index = {device_id: idx for idx, device_id in enumerate(devices_list)}

    # Define columns and their positions in the result tuple
    columns = {
        'avg_temperature': 2,
        'avg_humidity': 3,
        'pressure_amplitude': 4,
        'max_light': 5,
        'radar': 6,
        'sensor_min_s0': 7,
        'sensor_min_s1': 8,
        'sensor_min_s2': 9,
        'sensor_min_s3': 10,
        'sensor_min_s4': 11,
        'sensor_min_s5': 12,
        'sensor_min_s6': 13,
        'sensor_min_s7': 14,
        'sensor_min_s8': 15,
        'sensor_min_s9': 16
    }

    # Process each record
    for record in day_data:
        # Get minute and device_id from record
        record_time = record[0]  # minute column
        device_id = record[1]    # device_id column

        if record_time and device_id:
            # Calculate minute delta
            minute_delta = int((record_time - start_time).total_seconds() / 60)

            if 0 <= minute_delta < arr_source.shape[1]:
                # Calculate base index for this device
                base_idx = device_to_index[device_id] * len(columns)

                # Fill data for each sensor/measurement type
                for col_name, col_offset in columns.items():
                    value = record[col_offset]
                    if value is not None:  # Skip NULL values
                        row_idx = base_idx + list(columns.keys()).index(col_name)
                        arr_source[row_idx, minute_delta] = value

    return arr_source

def fast_fill_array_from_timescale_bad(day_data, time_from_str, devices_list, arr_source, timezone_str="Europe/Berlin"):
    """
    Optimized version of array filling from TimeScaleDB data.
    Uses vectorized operations for significant speed improvement.
    """
    # Convert start time to timezone-aware datetime
    start_time = datetime.datetime.strptime(time_from_str, '%Y-%m-%d %H:%M:%S%z')

    # Create device index mapping
    device_to_index = {device_id: idx for idx, device_id in enumerate(devices_list)}

    # Define column mappings (sensor type to position in record) - KEEP EXACT SAME ORDER as original
    columns = {
        'avg_temperature': 2,
        'avg_humidity': 3,
        'pressure_amplitude': 4,
        'max_light': 5,
        'radar': 6,
        'sensor_min_s0': 7,
        'sensor_min_s1': 8,
        'sensor_min_s2': 9,
        'sensor_min_s3': 10,
        'sensor_min_s4': 11,
        'sensor_min_s5': 12,
        'sensor_min_s6': 13,
        'sensor_min_s7': 14,
        'sensor_min_s8': 15,
        'sensor_min_s9': 16
    }

    # Pre-compute column keys list for consistent indexing
    column_keys = list(columns.keys())

    # Pre-process data into a more efficient structure
    # Group by device_id to reduce lookup operations
    device_data = defaultdict(list)
    for record in day_data:
        if record[0] and record[1]:  # If time and device_id exist
            device_data[record[1]].append(record)

    # Process each device's data in bulk
    for device_id, records in device_data.items():
        if device_id not in device_to_index:
            continue

        base_idx = device_to_index[device_id] * len(columns)

        # Convert records to numpy array for faster processing
        records_array = np.array(records, dtype=object)

        # Calculate all minute deltas at once
        times = records_array[:, 0]
        minute_deltas = np.array([(t - start_time).total_seconds() / 60 for t in times], dtype=int)

        # Filter valid minute deltas
        valid_mask = (minute_deltas >= 0) & (minute_deltas < arr_source.shape[1])
        if not np.any(valid_mask):
            continue

        minute_deltas = minute_deltas[valid_mask]
        records_array = records_array[valid_mask]

        # Process each column type in bulk
        for col_name, col_offset in columns.items():
            # Use pre-computed column_keys list for consistent indexing
            row_idx = base_idx + column_keys.index(col_name)
            values = records_array[:, col_offset]

            # Filter out None values
            valid_values = ~np.equal(values, None)
            if not np.any(valid_values):
                continue

            # Update array in bulk
            arr_source[row_idx, minute_deltas[valid_values]] = values[valid_values]

    return arr_source

def fast_fill_array_from_timescale(day_data, time_from_str, devices_list, arr_source, timezone_str="Europe/Berlin"):
    """
    Optimized version of array filling from TimeScaleDB data.
    Uses vectorized operations for significant speed improvement.
    """
    # Convert start time to timezone-aware datetime
    start_time = datetime.datetime.strptime(time_from_str, '%Y-%m-%d %H:%M:%S%z')

    # Create device index mapping
    device_to_index = {device_id: idx for idx, device_id in enumerate(devices_list)}

    # Define column mappings (sensor type to position in record) - KEEP EXACT SAME ORDER as original
    columns = {
        'avg_temperature': 2,
        'avg_humidity': 3,
        'pressure_amplitude': 4,
        'max_light': 5,
        'radar': 6,
        'sensor_min_s0': 7,
        'sensor_min_s1': 8,
        'sensor_min_s2': 9,
        'sensor_min_s3': 10,
        'sensor_min_s4': 11,
        'sensor_min_s5': 12,
        'sensor_min_s6': 13,
        'sensor_min_s7': 14,
        'sensor_min_s8': 15,
        'sensor_min_s9': 16
    }

    # Pre-compute column keys list for consistent indexing
    column_keys = list(columns.keys())

    # Pre-process data into a more efficient structure
    # Group by device_id to reduce lookup operations
    device_data = defaultdict(list)
    for record in day_data:
        if record[0] and record[1]:  # If time and device_id exist
            device_data[record[1]].append(record)

    # Process each device's data in bulk
    for device_id, records in device_data.items():
        if device_id not in device_to_index:
            continue

        base_idx = device_to_index[device_id] * len(columns)

        # Convert records to numpy array for faster processing
        records_array = np.array(records, dtype=object)

        # Calculate all minute deltas at once
        times = records_array[:, 0]
        minute_deltas = np.array([(t - start_time).total_seconds() / 60 for t in times], dtype=int)

        # Filter valid minute deltas
        valid_mask = (minute_deltas >= 0) & (minute_deltas < arr_source.shape[1])
        if not np.any(valid_mask):
            continue

        minute_deltas = minute_deltas[valid_mask]
        records_array = records_array[valid_mask]

        # Process each column type in bulk
        for col_name, col_offset in columns.items():
            # Use pre-computed column_keys list for consistent indexing
            row_idx = base_idx + column_keys.index(col_name)
            values = records_array[:, col_offset]

            # Filter out None values
            valid_values = ~np.equal(values, None)
            if not np.any(valid_values):
                continue

            # Update array in bulk
            arr_source[row_idx, minute_deltas[valid_values]] = values[valid_values]

    return arr_source

def CalcExtremes(arr_source, length, height):
    """
    Calculate min and max values for each row within legal bounds.
    Optimized version using numpy vectorized operations.

    Parameters:
    arr_source: numpy array of shape (height, length+4) containing data and bounds
    length: number of data points to process (typically 1440 for minutes in a day)
    height: number of rows in the array

    Returns:
    numpy array with min/max values stored in columns 1442 and 1443
    """
    # Extract the data portion and bounds
    data = arr_source[:, :length]
    ignore_below = arr_source[:, 1440:1441]  # Keep 2D shape for broadcasting
    ignore_above = arr_source[:, 1441:1442]  # Keep 2D shape for broadcasting

    # Create masks for valid values
    above_min_mask = data >= ignore_below
    below_max_mask = data <= ignore_above
    valid_mask = above_min_mask & below_max_mask

    # Create a masked array to handle invalid values
    masked_data = np.ma.array(data, mask=~valid_mask)

    # Calculate min and max values for each row
    row_mins = np.ma.min(masked_data, axis=1).filled(-0.001)
    row_maxs = np.ma.max(masked_data, axis=1).filled(-0.001)

    # Store results
    arr_source[:, 1442] = row_mins
    arr_source[:, 1443] = row_maxs

    return arr_source

def plot(arr, filename="histogram.png", title="Histogram Plot", figsize=(12, 6),
                  color='blue', style='histogram', bins=1000):
    """
    Plot a 1D numpy array as a line or scatter plot

    Parameters:
    arr : 1D numpy array
    title : str, plot title
    figsize : tuple, figure size in inches
    color : str, line/point color
    style : str, 'line' or 'scatter'
    """
    title = filename
    plt.figure(figsize=figsize)
    x = np.arange(len(arr))

    if style == 'line':
        x = np.arange(len(arr))
        plt.plot(x, arr, color=color)
    elif style == 'scatter':
        x = np.arange(len(arr))
        plt.scatter(x, arr, color=color, alpha=0.6)
    elif style == 'histogram':
        plt.hist(arr.ravel(), bins=bins, range=(0, 100), color=color, alpha=0.8)
        plt.yscale('log')  # Using log scale for better visualization
        plt.xlabel('Signal Value')
        plt.ylabel('Frequency')

    plt.title(title)
    plt.xlabel('Index')
    plt.ylabel('Value')
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig(filename)
    plt.close()
    print(f"Plot saved to: {filename}")
    #plt.show()

def ShowArray(arr, threshold, filename="histogram.png", title="Histogram Plot", figsize=(12, 6),
                  color='blue', style='histogram', bins=1000):
    """
    Plot a 1D numpy array as a line or scatter plot

    Parameters:
    arr : 1D numpy array
    title : str, plot title
    figsize : tuple, figure size in inches
    color : str, line/point color
    style : str, 'line' or 'scatter'
    """
    title = filename
    plt.figure(figsize=figsize)
    x = np.arange(len(arr))

    if style == 'line':
        x = np.arange(len(arr))
        plt.plot(x, arr, color=color)
        plt.axhline(y=threshold, color='red', linestyle='--',
                   label=f'Threshold: {threshold:.3f}')
        plt.xlabel('Index')
        plt.ylabel('Value')
    elif style == 'scatter':
        x = np.arange(len(arr))
        plt.scatter(x, arr, color=color, alpha=0.6)
    elif style == 'histogram':
        plt.hist(arr.ravel(), bins=bins, range=(0, 100), color=color, alpha=0.8)
        plt.yscale('log')  # Using log scale for better visualization
        plt.xlabel('Signal Value')
        plt.ylabel('Frequency')

    plt.title(title)
    plt.xlabel('Index')
    plt.ylabel('Value')
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig(filename)
    plt.close()
    print(f"Plot saved to: {filename}")
    #plt.show()

def AddLimits_optimized(arr_source, devices_c, sensors_c, percentile):
    """
    Vectorized version of AddLimits that processes all sensors at once.

    Parameters:
    arr_source: array of shape (devices_c * sensors_c, 1444)
    devices_c: number of devices
    sensors_c: number of sensors per device
    percentile: parameter for clean_data_vectorized
    """
    total_sensors = devices_c * sensors_c

    # Create arrays of sensor indices for all rows
    sensor_indices = np.arange(total_sensors) % sensors_c

    # Convert sensor_legal_values into arrays for vectorized access
    sensor_types = np.array([s_table[i] for i in range(sensors_c)])
    min_vals = np.array([sensor_legal_values[t][0] for t in sensor_types])
    max_vals = np.array([sensor_legal_values[t][1] for t in sensor_types])
    windows = np.array([sensor_legal_values[t][2] for t in sensor_types])

    # Get values for each row based on sensor type
    row_windows = windows[sensor_indices]
    row_mins = min_vals[sensor_indices]
    row_maxs = max_vals[sensor_indices]

    # Process rows that need cleaning (window > 2)
    clean_mask = row_windows > 2
    if np.any(clean_mask):
        # Clean each row with its corresponding window size
        for window in np.unique(row_windows[clean_mask]):
            # Get indices of rows that need this window size
            rows_to_clean = np.where(clean_mask & (row_windows == window))[0]

            # Clean each row individually (since clean_data_vectorized expects 1D input)
            for row_idx in rows_to_clean:
                arr_source[row_idx, :1440] = clean_data_vectorized(
                    arr_source[row_idx, :1440],
                    window,
                    percentile
                )

    # Set min/max values for all rows at once
    arr_source[:, 1440] = row_mins
    arr_source[:, 1441] = row_maxs

    return arr_source

def AddSmellLimits_optimized(arr_source, devices_c, sensors_c, percentile):
    """
    Vectorized version of AddLimits that processes all sensors at once.
    Parameters:
    arr_source: array of shape (devices_c * sensors_c, 1444)
    devices_c: number of devices
    sensors_c: number of sensors per device
    percentile: parameter for clean_data_vectorized
    """
    total_sensors = devices_c * sensors_c
    # Create arrays of sensor indices for all rows
    sensor_indices = np.arange(total_sensors) % sensors_c
    # Convert sensor_legal_values into arrays for vectorized access
    sensor_types = np.array([smells_table[i] for i in range(sensors_c)])
    min_vals = np.array([smell_legal_values[t][0] for t in sensor_types])
    max_vals = np.array([smell_legal_values[t][1] for t in sensor_types])
    # Get values for each row based on sensor type
    row_mins = min_vals[sensor_indices]
    row_maxs = max_vals[sensor_indices]

    # Replace values smaller than smell_min and larger than smell_max with no_smell
    # Create a mask for the data points (first 1440 columns)
    data_mask_below = arr_source[:, :1440] < smell_min
    data_mask_above = arr_source[:, :1440] > smell_max
    data_mask_invalid = data_mask_below | data_mask_above

    # Replace invalid values with no_smell
    arr_source[:, :1440][data_mask_invalid] = no_smell

    # Set min/max values for all rows at once
    arr_source[:, 1440] = row_mins
    arr_source[:, 1441] = row_maxs
    return arr_source

def AddLimits(arr_source, devices_c, sensors_c, percentile):
    for y in range(devices_c*sensors_c):
        sensor_index = y % sensors_c
        min_ok, max_ok, window = sensor_legal_values[s_table[sensor_index]]
        #if EnablePlot:
            #if (y == 33):
                #print("stop")
                #plot(arr_source[y, :1440], "before_clean_sensor.png")
        if window > 2:
            arr_source[y, :1440] = clean_data_vectorized(arr_source[y, :1440], window, percentile)

        #if EnablePlot:
            #if (y == 33):
                #print("stop")
                #plot(arr_source[y, :1440], "after_clean_sensor.png")

        arr_source[y][1440] = min_ok
        arr_source[y][1441] = max_ok
    return arr_source

def clean_data_vectorized(data, window, percentile):
    """
    Vectorized version of clean_data function using pure numpy
    First removes zeros, then cleans outliers

    Parameters:
    data: numpy array of sensor readings
    window: int, size of rolling window
    percentile: float, percentile threshold for deviation filtering
    """
    # Create a copy to avoid modifying original data
    working_data = data.copy()

    # Replace zeros with NaN
    zero_mask = working_data == 0
    working_data[zero_mask] = np.nan

    # Create rolling window view of the data
    def rolling_window(a, window):
        shape = a.shape[:-1] + (a.shape[-1] - window + 1, window)
        strides = a.strides + (a.strides[-1],)
        return np.lib.stride_tricks.as_strided(a, shape=shape, strides=strides)

    # Pad array for edge handling
    pad_width = window // 2
    padded = np.pad(working_data, pad_width, mode='edge')

    # Create rolling windows
    windows = rolling_window(padded, window)

    # Calculate rolling median (ignoring NaN values)
    medians = np.nanmedian(windows, axis=1)

    # Forward/backward fill any NaN in medians
    # Forward fill
    mask = np.isnan(medians)
    idx = np.where(~mask, np.arange(mask.shape[0]), 0)
    np.maximum.accumulate(idx, out=idx)
    medians[mask] = medians[idx[mask]]

    # Backward fill any remaining NaNs
    mask = np.isnan(medians)
    idx = np.where(~mask, np.arange(mask.shape[0]), mask.shape[0] - 1)
    idx = np.minimum.accumulate(idx[::-1])[::-1]
    medians[mask] = medians[idx[mask]]

    # Calculate deviations (ignoring NaN values)
    deviations = np.abs(working_data - medians)

    # Calculate threshold (ignoring NaN values)
    threshold = np.nanpercentile(deviations, percentile)

    # Create mask and replace outliers with median values
    # Points are good if they're not NaN and deviation is within threshold
    good_points = (~np.isnan(working_data)) & (deviations <= threshold)

    # Replace all bad points (including zeros and outliers) with median values
    result = np.where(good_points, working_data, medians)

    return result


def process_chunk(args):
    """
    Process a chunk of rows
    """
    chunk, sensors_c, sensor_legal_values, s_table, window, percentile = args
    result = np.copy(chunk)

    # Process all time series in the chunk at once
    result[:, :1440] = np.array([
        clean_data_vectorized(row[:1440], window, percentile)
        for row in chunk
    ])

    # Set limits for all rows in chunk using vectorized operations
    sensor_indices = np.arange(len(chunk)) % sensors_c
    min_values = np.array([sensor_legal_values[s_table[i]][0] for i in sensor_indices])
    max_values = np.array([sensor_legal_values[s_table[i]][1] for i in sensor_indices])

    result[:, 1440] = min_values
    result[:, 1441] = max_values

    return result


def FillImage_optimized(scaled_day, devices_c, sensors_c, arr_stretched, group_by, bw):
    """
    Optimized version of FillImage function that fills the stretched array with colored sensor data.

    Parameters:
    scaled_day: 2D array of shape (stripes, minutes+4) containing sensor readings
    devices_c: number of devices
    sensors_c: number of sensors per device
    arr_stretched: 3D array of shape (stripes*stretch_by, minutes, 3) to fill with RGB values
    group_by: grouping strategy ("sensortype" or other)
    bw: boolean flag for black and white output

    Returns:
    arr_stretched: Filled array with RGB values
    and vocs_scaled aray from 0 to 1280
    """
    stripes = devices_c * sensors_c
    stretch_by = arr_stretched.shape[0] // stripes
    minutes = arr_stretched.shape[1]

    # Pre-calculate VOC rows mask
    if group_by != "sensortype":
        voc_rows = np.arange(stripes) >= 5 * devices_c
    else:
        voc_rows = (np.arange(stripes) % sensors_c) >= 5

    # Pre-calculate destination row mapping for sensortype grouping
    if group_by == "sensortype":
        row_indices = np.arange(stripes)
        sensor_indices = row_indices % sensors_c
        device_indices = row_indices // sensors_c
        dest_rows = sensor_indices * devices_c + device_indices
        dest_rows = dest_rows[:, np.newaxis] * stretch_by + np.arange(stretch_by)
    else:
        row_indices = np.arange(stripes)[:, np.newaxis] * stretch_by + np.arange(stretch_by)

    # Optimize color calculation functions
    def best_color_vectorized(vals):
        """Vectorized version of BestColor that matches the original implementation exactly"""
        vals = np.clip(vals, 0, 1279).astype(np.int32)
        r = np.zeros_like(vals, dtype=np.int32)
        g = np.zeros_like(vals, dtype=np.int32)
        b = np.zeros_like(vals, dtype=np.int32)

        # Region 0-255
        mask1 = vals < 256
        r[mask1] = 255
        g[mask1] = vals[mask1]

        # Region 256-511
        mask2 = (vals >= 256) & (vals < 512)
        r[mask2] = 511 - vals[mask2]
        g[mask2] = 255

        # Region 512-767
        mask3 = (vals >= 512) & (vals < 768)
        g[mask3] = 255
        b[mask3] = vals[mask3] - 512

        # Region 768-1023
        mask4 = (vals >= 768) & (vals < 1024)
        g[mask4] = 1023 - vals[mask4]
        b[mask4] = 255

        # Region 1024-1279
        mask5 = vals >= 1024
        r[mask5] = vals[mask5] - 1024
        b[mask5] = 255

        return r, g, b

    def gray_color_vectorized(vals):
        """Vectorized version of GrayColor that matches the original implementation exactly"""
        vals = np.clip(vals, 0, 255).astype(np.int32)
        return vals, vals, vals

    color_func = gray_color_vectorized if bw else best_color_vectorized

    # Process all rows at once
    valid_mask = scaled_day[:, :minutes] != -0.001
    big_min = scaled_day[:, 1442:1443]  # Keep 2D shape for broadcasting
    big_max = scaled_day[:, 1443:1444]

    # Calculate k factors where max > min
    valid_range_mask = big_max > big_min
    k = np.zeros_like(big_min)
    k[valid_range_mask] = (1280 if not bw else 255) / (big_max[valid_range_mask] - big_min[valid_range_mask])

    # Calculate normalized values for all rows at once
    normalized_vals = np.zeros_like(scaled_day[:, :minutes])
    valid_range_indices = np.where(valid_range_mask)[0]

    normalized_vals[valid_range_indices] = (
        k[valid_range_indices] *
        (scaled_day[valid_range_indices, :minutes] - big_min[valid_range_indices])
    )

    # Invert VOC rows
    normalized_vals[voc_rows] = (1279 if not bw else 255) - normalized_vals[voc_rows]

    # Apply valid mask
    normalized_vals[~valid_mask] = 0

    # Convert to RGB
    r, g, b = color_func(normalized_vals)

    # Create RGB array
    rgb_values = np.stack([r, g, b], axis=-1)

    # Handle special case where max == min
    equal_range_mask = ~valid_range_mask
    if np.any(equal_range_mask):
        rgb_values[equal_range_mask.ravel()] = 128

    # Fill the stretched array efficiently
    if group_by == "sensortype":
        arr_stretched[dest_rows] = rgb_values[:, None]
    else:
        arr_stretched[row_indices] = rgb_values[:, None]

    return arr_stretched, normalized_vals[voc_rows]


def FillSmellImage_optimized(scaled_day, arr_stretched, y_offset):
    """
    Fill the stretched array with colored sensor data from scaled_day.

    Parameters:
    scaled_day: 2D array of shape (70, 1444) containing sensor readings
    arr_stretched: 3D array of shape (2685, 1640, 3) to fill with RGB values

    Returns:
    arr_stretched: Filled array with RGB values
    """
    stretch_by = 20
    x_offset = 200

    def best_color_vectorizedS(vals):
        """Vectorized version of BestColor that matches the original implementation exactly"""
        vals = np.clip(vals, 0, 1279).astype(np.int32)
        r = np.zeros_like(vals, dtype=np.int32)
        g = np.zeros_like(vals, dtype=np.int32)
        b = np.zeros_like(vals, dtype=np.int32)

        # Region 0-255
        mask1 = vals < 256
        r[mask1] = 255
        g[mask1] = vals[mask1]

        # Region 256-511
        mask2 = (vals >= 256) & (vals < 512)
        r[mask2] = 511 - vals[mask2]
        g[mask2] = 255

        # Region 512-767
        mask3 = (vals >= 512) & (vals < 768)
        g[mask3] = 255
        b[mask3] = vals[mask3] - 512

        # Region 768-1023
        mask4 = (vals >= 768) & (vals < 1024)
        g[mask4] = 1023 - vals[mask4]
        b[mask4] = 255

        # Region 1024-1279
        mask5 = vals >= 1024
        r[mask5] = vals[mask5] - 1024
        b[mask5] = 255

        return r, g, b

    # Process each row in scaled_day
    for row_idx in range(scaled_day.shape[0]):
        # Extract min and max for this row
        row_min = scaled_day[row_idx, 1442]
        row_max = scaled_day[row_idx, 1443]

        # Get data for this row (first 1440 elements)
        row_data = scaled_day[row_idx, :1440]

        # Check if min and max are the same
        if row_min == row_max:
            # Create gray stripe
            stripe = np.ones((stretch_by, 1440, 3), dtype=np.int32) * 128
        else:
            # Normalize the data between 0 and 1279
            k = 1280 / (row_max - row_min)
            normalized_vals = k * (row_data - row_min)
            normalized_vals = np.clip(normalized_vals, 0, 1279)

            # Convert to RGB
            r, g, b = best_color_vectorizedS(normalized_vals)

            # Create RGB stripe
            stripe = np.zeros((stretch_by, 1440, 3), dtype=np.int32)

            # Fill stripe with the same color pattern for all stretch_by rows
            for i in range(stretch_by):
                stripe[i, :, 0] = r
                stripe[i, :, 1] = g
                stripe[i, :, 2] = b

        # Calculate the y position for this stripe
        y_pos = y_offset + row_idx * stretch_by

        # Place the stripe into arr_stretched
        print(stretch_by, stripe.shape, arr_stretched.shape, y_pos)
        arr_stretched[y_pos:y_pos+stretch_by, x_offset:x_offset+1440, :] = stripe

    return arr_stretched

def FillImage(scaled_day, devices_c, sensors_c, arr_stretched, group_by, bw):
    """
    Fill the stretched array with colored sensor data.

    Parameters:
    scaled_day: 2D array of shape (stripes, minutes+4) containing sensor readings
    devices_c: number of devices
    sensors_c: number of sensors per device
    arr_stretched: 3D array of shape (stripes*stretch_by, minutes, 3) to fill with RGB values

    Returns:
    arr_stretched: Filled array with RGB values
    """
    stripes = devices_c * sensors_c
    stretch_by = arr_stretched.shape[0] // stripes
    minutes = arr_stretched.shape[1]

    # Create a boolean mask for VOC sensors
    if group_by != "sensortype":
        voc_rows = np.array([i for i in range(stripes) if int(i/devices_c) >= 5])
    else:
        voc_rows = np.array([i for i in range(stripes) if int(i % sensors_c) >= 5])
    # Vectorize the BestColor function
    if not bw:
        vectorized_best_color = np.vectorize(BestColor)
    else:
        vectorized_best_color = np.vectorize(GrayColor)

    # Process each row
    for row in range(stripes):

        row_data = scaled_day[row, :minutes]  # Get minute data

        #if row == 33:
        #    print("stop")
        #    plot(row_data, "row_data.png")
        big_min = scaled_day[row, 1442]  # min value
        big_max = scaled_day[row, 1443]  # max value

        # Create mask for valid values
        valid_mask = row_data != -0.001

        # Initialize RGB row with zeros
        rgb_row = np.zeros((minutes, 3), dtype=np.uint8)

        if big_max > big_min:
            # Scale factor
            if not bw:
                k = 1280/(big_max-big_min)
            else:
                k = 255/(big_max-big_min)
            # Calculate normalized values
            normalized_vals = k * (row_data - big_min)

            # Invert if it's a VOC row
            if row in voc_rows:
                if not bw:
                    normalized_vals = 1279 - normalized_vals
                else:
                    normalized_vals = 255 - normalized_vals

            # Apply valid mask
            normalized_vals = np.where(valid_mask, normalized_vals, 0)
            #if row == 33:
            #    plot(normalized_vals, "normalized_vals.png")

            # Convert to RGB colors (vectorized)
            r, g, b = vectorized_best_color(normalized_vals)

            # Combine into RGB array
            rgb_row[valid_mask] = np.stack([r[valid_mask],
                                          g[valid_mask],
                                          b[valid_mask]], axis=1)
        else:
            # Set to gray where valid
            rgb_row[valid_mask] = 128


        if group_by == "sensortype":

            # Fill the stretched rows
            sensor_index = row % sensors_c
            device_index = int(row/sensors_c)
            dest_row = sensor_index * devices_c + device_index  #0-0, 1-
            start_idx = dest_row * stretch_by
            end_idx = start_idx + stretch_by
            arr_stretched[start_idx:end_idx] = rgb_row
        else:
            # Fill the stretched rows
            start_idx = row * stretch_by
            end_idx = start_idx + stretch_by
            arr_stretched[start_idx:end_idx] = rgb_row

    return arr_stretched

def FillRadarImage(scaled_day, devices_c, bands, arr_stretched, group_by, map_type):
    """
    Fill the stretched array with colored sensor data.

    Parameters:
    scaled_day: 2D array of shape (stripes, minutes+4) containing sensor readings
    devices_c: number of devices
    bands: number of bands per device
    arr_stretched: 3D array of shape (stripes*stretch_by, minutes, 3) to fill with RGB values

    Returns:
    arr_stretched: Filled array with RGB values
    """
    stripes = devices_c * bands
    stretch_by = arr_stretched.shape[0] // stripes
    minutes = arr_stretched.shape[1]

    # Create a boolean mask for VOC sensors
    if group_by != "sensortype":
        voc_rows = np.array([i for i in range(stripes) if int(i/devices_c) >= 5])
    else:
        voc_rows = np.array([i for i in range(stripes) if int(i % bands) >= 5])
    # Vectorize the BestColor function
    if map_type == 3:
        vectorized_best_color = np.vectorize(BestColor)
    else:
        vectorized_best_color = np.vectorize(GrayColor)

    # Process each row
    for row in range(stripes):

        row_data = scaled_day[row, :minutes]  # Get minute data

        #if row == 33:
        #    print("stop")
        #    plot(row_data, "row_data.png")
        big_min = 0 #scaled_day[row, 1442]  # min value
        big_max = 255 #scaled_day[row, 1443]  # max value

        # Create mask for valid values
        valid_mask = row_data != -0.001

        # Initialize RGB row with zeros
        rgb_row = np.zeros((minutes, 3), dtype=np.uint8)

        if big_max > big_min:
            # Scale factor
            if map_type == 3:
                k = 1280/(big_max-big_min)
            else:
                k = 255/(big_max-big_min)
            # Calculate normalized values
            normalized_vals = k * (row_data - big_min)

            # Invert if it's a VOC row
            if row in voc_rows:
                if map_type == 3:
                    normalized_vals = 1279 - normalized_vals
                else:
                    normalized_vals = 255 - normalized_vals

            # Apply valid mask
            normalized_vals = np.where(valid_mask, normalized_vals, 0)
            #if row == 33:
            #    plot(normalized_vals, "normalized_vals.png")

            # Convert to RGB colors (vectorized)
            r, g, b = vectorized_best_color(normalized_vals)

            # Combine into RGB array
            rgb_row[valid_mask] = np.stack([r[valid_mask],
                                          g[valid_mask],
                                          b[valid_mask]], axis=1)
        else:
            # Set to gray where valid
            rgb_row[valid_mask] = 128


        if group_by == "sensortype":

            # Fill the stretched rows
            band_index = row % bands
            device_index = int(row/bands)
            dest_row = band_index * devices_c + device_index  #0-0, 1-
            start_idx = dest_row * stretch_by
            end_idx = start_idx + stretch_by
            arr_stretched[start_idx:end_idx] = rgb_row
        else:
            # Fill the stretched rows
            start_idx = row * stretch_by
            end_idx = start_idx + stretch_by
            arr_stretched[start_idx:end_idx] = rgb_row

    return arr_stretched

def  GetFullLocMapDetails(map_file):
    #'/Volumes/XTRM-Q/wellnuo/daily_maps/1/1_2023-11-07_dayly_image.png'
    local_timezone = pytz.timezone('America/Los_Angeles')  # Replace with your local timezone
    dest_path = os.path.dirname(map_file)

    parts = map_file.split("/")
    deployment = parts[-2]
    parts1 = parts[-1].split("_")
    date_string = parts1[1]

    deployments = GetDeploymentB(deployment, -1) #All
    last_locations_file = ""
    last_per_minute_file = ""

    today = datetime.today()
    deployment_details = deployments[0]
    deployment_pair = deployment_details[0]
    proximity_lst = deployment_details[1]

    date_object = datetime.strptime(date_string, "%Y-%m-%d")
    date_object_midnight = local_timezone.localize(date_object.replace(hour=0, minute=0, second=0, microsecond=0))
    selected_epoch = int(date_object_midnight.timestamp())

    sel_date = datetime.fromtimestamp(selected_epoch)
    devices_list_str = GetDevicesList(deployment_details, sel_date)#.split(',')
    devices_list = ast.literal_eval(devices_list_str)
    return devices_list, selected_epoch, dest_path

def median_filter(data, window_size):
    filtered_data = []
    print(len(data))
    window = deque(maxlen=window_size)
    last_value = -1
    offset = 0
    added_old = 0
    for value in data:
        if value != '':
            added_old = 0
            last_value = value
            window.append(value)
            if len(window) == window_size:
                # Sort the window and get the median value
                sorted_window = sorted(window)
                median = sorted_window[window_size // 2]
                filtered_data.append(median)
        else:
            if last_value != -1:
                if added_old < window_size:
                    added_old = added_old + 1
                    window.append(last_value)
                else:
                    window.append(-1)

                if len(window) == window_size:
                    # Sort the window and get the median value
                    sorted_window = sorted(window)
                    median = sorted_window[window_size // 2]
                    filtered_data.append(median)

            else:
                offset +=1

    if len(filtered_data) > 0:
        offset += (window_size // 2)
        #if starts empty, just leav it such, do not fake backwards from midnight
        first_val = -1# filtered_data[0]
        last_val = filtered_data[-1]
        front_padding = [first_val] * offset
        remaining = len(data) - len(filtered_data) - len(front_padding)
        back_padding =  [last_val] * remaining
        out_data = front_padding + filtered_data + back_padding
    else:
        out_data = data
    #add front and back padding

    return out_data

def FilterGlitches(wave_in, filter_minutes):

    if(filter_minutes > 0):
        notfiltered_wave = [i[0] for i in wave_in]
        filtered_wave = median_filter(notfiltered_wave, filter_minutes)
        for i, value in enumerate(filtered_wave):
            wave_in[i][0] = value

    return wave_in

def setup_timezone_converter(time_zone_st):
    """
    Setup timezone converter to be reused

    Parameters:
    time_zone_st (str): Timezone string (e.g. 'Europe/Berlin')

    Returns:
    pytz.timezone: Timezone object for conversion
    """
    return pytz.timezone(time_zone_st)

def ReadDailyRadar(MAC, current_date):

    #This will return all 1 Minute radar data for each gate in the file
    #Will return list (2 items) of lists: Maxes, Mins

    time_from_str, time_to_str = GetLocalTimeForDate(selected_date, time_zone_s)

    start_of_day = ToLocal(calendar.timegm(datetime(current_date.year, current_date.month,current_date.day, 0, 0).timetuple()))
    end_of_day = start_of_day + 1440 * 60
    file = os.path.join(scriptDir, "DB/"+MAC.upper() +"_"+str(current_date.year)+"_"+str(current_date.month).rjust(2, '0')+".db")
    file = file.replace("\\","/")
    file1 = os.path.join(scriptDir, "DB/processed_db/"+MAC.upper() +"_"+str(current_date.year)+"_"+str(current_date.month).rjust(2, '0')+".db")
    file1 = file1.replace("\\","/")
    if (not path.exists(file) and not path.exists(file1)):
        print(file + " and " + file1 + " are not found")
        return []
    result = []
    min_OK = "0"
    sqlr = "SELECT * FROM radars WHERE time >= "+str(start_of_day) +" and time < "+str(end_of_day) +" ORDER BY time ASC"
    #sqlr = "SELECT Date, high, low from "+sensor.lower()+"s1Min"+" WHERE low >= "+min_OK+" and Date >= "+str(start_of_day) +" and Date < "+str(end_of_day)
    print(sqlr)
    if os.path.exists(file):
        result = QuerrySql(file, sqlr)
    elif os.path.exists(file1):
        result = QuerrySql(file1, sqlr)

    #                    M0 ............M8 S2 ........S8
    #day_minutes_data = [[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]] * (24 * 60 + 2)
    day_minutes_data = [[0] * 16 for _ in range(24 * 60)]
    #for each gate lets find maximum value per minute
    for mgate in range(9):
        max_per_min = 0
        for minute_data in result:
            seconde = minute_data[0]
            date_time_minute = datetime.fromtimestamp(seconde)
            minute_m = 60*date_time_minute.hour+date_time_minute.minute
            if minute_data[mgate + 6] > day_minutes_data[minute_m][mgate]:
                day_minutes_data[minute_m][mgate] = minute_data[mgate + 6]

    for sgate in range(7):
        for minute_data in result:
            seconde = minute_data[0]
            date_time_minute = datetime.fromtimestamp(seconde)
            minute_m = 60*date_time_minute.hour+date_time_minute.minute
            if minute_data[sgate + 17] > day_minutes_data[minute_m][sgate+9]:
                day_minutes_data[minute_m][sgate+9] = minute_data[sgate + 17]

    return day_minutes_data


def FromLocalMidnight(epoch_time, local_delta):

    # Convert epoch time to UTC datetime object
    print(type(epoch_time))
    print(epoch_time)
    local_datetime = datetime.datetime.utcfromtimestamp(epoch_time+local_delta).replace(tzinfo=pytz.UTC)

    # Calculate minute count from midnight
    minutes_from_midnight = (local_datetime - local_datetime.replace(hour=0, minute=0, second=0, microsecond=0)).total_seconds() / 60
    return minutes_from_midnight

def process_wave_data_numpy(image_file, my_data, time_zone_s, device_id_2_threshold, radar_fields_of_interest):
    """
    NumPy-based version of wave processing

    Parameters:
    my_data: List of tuples containing (time_val, device_id, other radar_fields_of_interest)
    time_zone_s: Target timezone string
    device_id_2_threshold: Dictionary mapping device_ids to their thresholds

    Returns:
    List of [device_id, max_val] pairs for each minute
    """
    wave_m = None
    tz = pytz.timezone(time_zone_s)
    if not my_data:
        return [["", -1] for _ in range(1440)]

    vectorized_BestColor = np.vectorize(BestColor)
    stripes = len(device_id_2_threshold)
    stretch_by = 5
    minutes = 1440
    arr_source = np.zeros((int(stripes), minutes), dtype=np.float32)
    arr_stretched = np.zeros((int(stripes*stretch_by), minutes, 3), dtype=np.uint8)  #array to be written as image 3 for RGB channels


    index_map = {word: idx for idx, word in enumerate(radar_fields_of_interest)}
    devices_map = {word: idx for idx, word in enumerate(device_id_2_threshold)}
    times = []
    start_time = 0
    for data_set in my_data:
        time_stamp = data_set[0]
        if start_time == 0:

            # Convert timestamp to a datetime object in UTC
            local_tz = pytz.timezone(time_zone_s)
            local_time = time_stamp.astimezone(local_tz)
            # Set the time to the start of the day in the local time zone
            start_of_day_local = local_time.replace(hour=0, minute=0, second=0, microsecond=0)
            # Convert the start of the day back to UTC
            start_time = start_of_day_local.astimezone(pytz.utc)

        diff = time_stamp - start_time
        minute = int(diff.total_seconds() / 60)
        device_id =  data_set[1]
        field_name = device_id_2_threshold[device_id][0]
        field_index = index_map[field_name]
        threshold = device_id_2_threshold[device_id][1]
        value = data_set[2+field_index]
        if value > threshold:
            arr_source[devices_map[device_id]][minute] = value
    #np.savetxt('output.csv', arr_source, delimiter=',')
    if False:
        for yy in range(stripes):
            rgb_row = vectorized_BestColor(1280*arr_source[yy]/100)
            rgb_reshaped = np.array(rgb_row).reshape(3, minutes).T
            for stretch_index in range(stretch_by):
                y = yy * stretch_by + stretch_index
                arr_stretched[y, :] = rgb_reshaped

        SaveImageInBlob(image_file, arr_stretched, [])


    max_values = np.max(arr_source, axis=0)

    # Get indices (0-based)
    wave_m = np.argmax(arr_source, axis=0)

    # Add 1 to convert to 1-based indexing
    wave_m = wave_m + 1

    # Set to 0 where the column was all zeros
    wave_m[max_values == 0] = 0

    return wave_m



def ReadDailyCollapsedFastRadar(MAC, time_from_str, time_to_str):

    #This will return all 1 Minute radar data for each gate in the file
    #Will return list (2 items) of lists: Maxes, Mins based on s28 (stationary[2] to [8])
    radar_part = "(s2+s3+s4+s5+s6+s7+s8)/7"
    result = []
    min_OK = "0"
    sqlr = "SELECT radar_max FROM devices WHERE MAC = '"+MAC +"'"
    print(sqlr)
    DB_to_be_found_in_full = os.path.join(scriptDir, "main.db")
    DB_to_be_found_in_full = DB_to_be_found_in_full.replace("\\","/")
    result = QuerrySql(DB_to_be_found_in_full, sqlr)
    sqlr = "SELECT date, low FROM radars1Min WHERE date >= "+str(start_of_day) +" and date < "+str(end_of_day) + " ORDER BY date"
    if len(result)>0:
        if result[0][0] == 1:
            sqlr = "SELECT date, high FROM radars1Min WHERE date >= "+str(start_of_day) +" and date < "+str(end_of_day) + " ORDER BY date"

    print(sqlr)
    if os.path.exists(file):
        result = QuerrySql(file, sqlr)
    elif os.path.exists(file1):
        result = QuerrySql(file1, sqlr)

    return result

def vectorized_best_color_numpy(values):
    """Vectorized version of BestColor using pure NumPy"""
    # Ensure values are within range
    values = np.clip(values, 0, 1279)

    # Initialize output arrays
    r = np.zeros_like(values, dtype=np.uint8)
    g = np.zeros_like(values, dtype=np.uint8)
    b = np.zeros_like(values, dtype=np.uint8)

    # Create masks for each range
    mask_0_255 = values < 256
    mask_256_511 = (values >= 256) & (values < 512)
    mask_512_767 = (values >= 512) & (values < 768)
    mask_768_1023 = (values >= 768) & (values < 1024)
    mask_1024_plus = values >= 1024

    # Set values for each range using masks
    r[mask_0_255] = 255
    g[mask_0_255] = values[mask_0_255]

    r[mask_256_511] = 511 - values[mask_256_511]
    g[mask_256_511] = 255

    g[mask_512_767] = 255
    b[mask_512_767] = values[mask_512_767] - 512

    g[mask_768_1023] = 1023 - values[mask_768_1023]
    b[mask_768_1023] = 255

    r[mask_1024_plus] = values[mask_1024_plus] - 1024
    b[mask_1024_plus] = 255

    return np.stack([r, g, b], axis=-1)

def create_optimized_heatmap(my_data, bw, fields, wave_m, device_to_index, base_minute, timezone_st, min_val, max_val):

    if len(my_data) < 1:
        return []


    local_tz = pytz.timezone(timezone_st)

    n_fields = len(fields)
    # Convert my_data to numpy array for faster processing
    data_array = np.array(my_data)

    # Get unique device IDs and create mapping

    # Convert device IDs to indices using vectorized operation
    device_indices = np.vectorize(device_to_index.get)(data_array[:, 1])

    # Calculate x coordinates (minutes from base)
    #minute is in local time zone, and base_minute is UTC
    base_minute_local =  base_minute #.astimezone(local_tz)
    #x_coords = np.array([(minute.replace(tzinfo=datetime.timezone.utc) - base_minute_local).total_seconds()/60 for minute in data_array[:, 0]], dtype=np.int32)
    x_coords = np.array([(minute - base_minute_local).total_seconds()/60 for minute in data_array[:, 0]], dtype=np.int32)

    # Extract values and convert to float
    values = data_array[:, 2:].astype(np.float32)

    if bw:
        # Process in batches to avoid memory issues
        batch_size = 1000
        for start_idx in range(0, len(data_array), batch_size):
            end_idx = min(start_idx + batch_size, len(data_array))
            batch_slice = slice(start_idx, end_idx)


            # Calculate gray values
            gray_values = (values[batch_slice, :] - min_val / (max_val - min_val)) * 255.0

            # Clip values to valid range
            gray_values = np.clip(gray_values, 0, 255).astype(np.uint8)

            # Create y coordinates for each record
            y_coords = (device_indices[batch_slice] * n_fields).reshape(-1, 1) + np.arange(n_fields)

            # Assign values to the image array
            for i in range(end_idx - start_idx):
                wave_m[y_coords[i], x_coords[batch_slice][i]] = gray_values[i, :, np.newaxis]

    else:  # Color mode
        # Process in batches
        batch_size = 1000
        for start_idx in range(0, len(data_array), batch_size):
            end_idx = min(start_idx + batch_size, len(data_array))
            batch_slice = slice(start_idx, end_idx)

            # Calculate color values
            color_values = np.zeros_like(values[batch_slice])

            color_values[:, :] = ((values[batch_slice, :] - min_val) / (max_val - min_val)) * 1279.0
            #color_values[:, :] = (values[batch_slice, :] / 100.0) * 1279.0  # other fields

            # Create y coordinates for each record
            y_coords = (device_indices[batch_slice] * n_fields).reshape(-1, 1) + np.arange(n_fields)

            # Convert to RGB colors
            for i in range(end_idx - start_idx):
                rgb_values = vectorized_best_color_numpy(color_values[i])
                wave_m[y_coords[i], x_coords[batch_slice][i]] = rgb_values

    return wave_m

def create_light_optimized_heatmap(my_data, bw, fields, wave_m, device_to_index, base_minute, timezone_st, min_val=0, max_val=4095):
    """
    Create an optimized heatmap for light data (range 0-4095)

    Parameters:
    my_data (list): Data from the database query
    bw (bool): Whether to create a black and white (True) or color (False) heatmap
    fields (list): List of field names
    wave_m (numpy.ndarray): The image array to fill
    device_to_index (dict): Mapping from device_id to index
    base_minute (datetime): The base minute for time calculations
    timezone_st (str): Timezone string
    min_val (float): Minimum value for normalization (default: 0)
    max_val (float): Maximum value for normalization (default: 4095)

    Returns:
    numpy.ndarray: The filled image array
    """
    if len(my_data) < 1:
        return wave_m

    import numpy as np
    import pytz

    # Get the local timezone
    local_tz = pytz.timezone(timezone_st)

    # Number of fields (should be 1 for light data)
    n_fields = len(fields)

    # Convert my_data to numpy array for faster processing
    data_array = np.array(my_data)

    # Convert device IDs to indices using vectorized operation
    device_indices = np.vectorize(device_to_index.get)(data_array[:, 1])

    # Calculate x coordinates (minutes from base)
    x_coords = np.array([(minute - base_minute).total_seconds()/60 for minute in data_array[:, 0]], dtype=np.int32)

    # Extract values and convert to float - light data is in column 2
    # Reshape to match expected format (n_samples, n_fields)
    values = data_array[:, 2].astype(np.float32).reshape(-1, 1)

    # Process in batches to avoid memory issues
    batch_size = 1000

    if bw:
        for start_idx in range(0, len(data_array), batch_size):
            end_idx = min(start_idx + batch_size, len(data_array))
            batch_slice = slice(start_idx, end_idx)

            # Normalize light values (0-4095) to grayscale (0-255)
            gray_values = ((values[batch_slice] - min_val) / (max_val - min_val) * 255.0)

            # Clip values to valid range
            gray_values = np.clip(gray_values, 0, 255).astype(np.uint8)

            # Create y coordinates for each record
            y_coords = (device_indices[batch_slice] * n_fields).reshape(-1, 1)

            # Assign values to the image array
            for i in range(end_idx - start_idx):
                # Create RGB grayscale (same value for R, G, B)
                gray_rgb = np.full(3, gray_values[i, 0], dtype=np.uint8)
                wave_m[y_coords[i, 0], x_coords[batch_slice][i]] = gray_rgb
    else:
        # Color mode
        for start_idx in range(0, len(data_array), batch_size):
            end_idx = min(start_idx + batch_size, len(data_array))
            batch_slice = slice(start_idx, end_idx)

            # Normalize light values (0-4095) to color range (0-1279)
            color_values = ((values[batch_slice] - min_val) / (max_val - min_val) * 1279.0)

            # Create y coordinates for each record
            y_coords = (device_indices[batch_slice] * n_fields).reshape(-1, 1)

            # For each value, calculate its RGB color and assign to the image
            for i in range(end_idx - start_idx):
                # Convert normalized value to RGB using vectorized_best_color_numpy
                rgb_value = vectorized_best_color_numpy(np.array([color_values[i, 0]]))[0]
                wave_m[y_coords[i, 0], x_coords[batch_slice][i]] = rgb_value

    return wave_m

def create_temperature_optimized_heatmap(my_data, bw, fields, wave_m, device_to_index, base_minute, timezone_st, min_val=0, max_val=4095):
    """
    Create an optimized heatmap for temperature data with alarm levels

    Parameters:
    my_data (list): Data from the database query with columns for minute, device_id, temperature_avg, alarm_level
    bw (bool): Whether to create a black and white (True) or color (False) heatmap
    fields (list): List of field names - should be ['temperature', 'temperature_state']
    wave_m (numpy.ndarray): The image array to fill
    device_to_index (dict): Mapping from device_id to index
    base_minute (datetime): The base minute for time calculations
    timezone_st (str): Timezone string
    min_val (float): Minimum value for temperature normalization
    max_val (float): Maximum value for temperature normalization

    Returns:
    numpy.ndarray: The filled image array
    """
    if len(my_data) < 1:
        return wave_m

    import numpy as np
    import pytz

    # Get the local timezone
    local_tz = pytz.timezone(timezone_st)

    # Number of fields (should be 2 for temperature data: temperature and alarm state)
    n_fields = len(fields)

    # Convert my_data to numpy array for faster processing
    data_array = np.array(my_data)

    # Convert device IDs to indices using vectorized operation
    device_indices = np.vectorize(device_to_index.get)(data_array[:, 1])

    # Calculate x coordinates (minutes from base)
    x_coords = np.array([(minute - base_minute).total_seconds()/60 for minute in data_array[:, 0]], dtype=np.int32)

    # Process in batches to avoid memory issues
    batch_size = 1000

    for start_idx in range(0, len(data_array), batch_size):
        end_idx = min(start_idx + batch_size, len(data_array))
        batch_slice = slice(start_idx, end_idx)

        for i in range(end_idx - start_idx):
            # Get data for this record
            temperature = data_array[batch_slice][i, 2]
            alarm_level = 0

            # If we have an alarm_level column (index 3), use it
            if data_array.shape[1] > 3:
                alarm_level = data_array[batch_slice][i, 3]

            # Calculate base y-coordinate for this device
            base_y = device_indices[batch_slice][i] * n_fields

            # Temperature row (even row - index 0, 2, 4...)
            # Normalize temperature to the color range and create color
            if not bw:
                # For color mode
                normalized_temp = np.clip((temperature - min_val) / (max_val - min_val) * 1279.0, 0, 1279)
                temp_rgb = vectorized_best_color_numpy(np.array([normalized_temp]))[0]
            else:
                # For B&W mode
                normalized_temp = np.clip((temperature - min_val) / (max_val - min_val) * 255.0, 0, 255)
                gray_value = int(normalized_temp)
                temp_rgb = np.array([gray_value, gray_value, gray_value], dtype=np.uint8)

            # Set the temperature color in the even row
            wave_m[base_y, x_coords[batch_slice][i]] = temp_rgb

            # Alarm level row (odd row - index 1, 3, 5...)
            # Set color based on alarm level (0=green, 1=yellow, 2=red)
            if alarm_level == 0:
                # Green for normal
                alarm_rgb = np.array([0, 255, 0], dtype=np.uint8)
            elif alarm_level == 1:
                # Yellow for warning
                alarm_rgb = np.array([0, 255, 255], dtype=np.uint8)
            else:  # alarm_level == 2
                # Red for critical
                alarm_rgb = np.array([0, 0, 255], dtype=np.uint8)

            # Set the alarm color in the odd row
            wave_m[base_y + 1, x_coords[batch_slice][i]] = alarm_rgb

    return wave_m


def create_humidity_optimized_heatmap(my_data, bw, fields, wave_m, device_to_index, base_minute, timezone_st, min_val=0, max_val=100):
    """
    Create a heatmap with the exact blue-cyan-green-yellow-red-violet spectrum
    matching Image 2, with green at position 40
    """
    if len(my_data) < 1:
        return wave_m

    # Number of fields
    n_fields = len(fields)

    # Convert my_data to numpy array for faster processing
    data_array = np.array(my_data)

    # Convert device IDs to indices using vectorized operation
    device_indices = np.vectorize(device_to_index.get)(data_array[:, 1])

    # Calculate x coordinates (minutes from base)
    x_coords = np.array([(minute - base_minute).total_seconds()/60 for minute in data_array[:, 0]], dtype=np.int32)

    # Process in batches to avoid memory issues
    batch_size = 1000

    # Define the color mapping function based on the exact spectrum we want
    def get_color(t):
        """Get RGB color from humidity 0-100"""
        # Define color stops - exact RGB values at each step
        # Format: (position, (r, g, b))
        #color_stops = [
            #(0, (0, 0, 255)),      # Blue
            #(20, (0, 255, 255)),   # Cyan
            #(40, (0, 255, 0)),     # Green (centered at 40)
            #(60, (255, 255, 0)),   # Yellow
            #(80, (255, 0, 0)),     # Red
            #(100, (255, 0, 255))   # Violet
        #]

        color_stops = [
            (0, (0, 0, 255)),      # Blue
            (16, (0, 255, 255)),   # Cyan
            (32, (0, 255, 0)),     # Green (now centered at 32)
            (60, (255, 255, 0)),   # Yellow
            (80, (255, 0, 0)),     # Red
            (100, (255, 0, 255))   # Violet
        ]

        # Ensure t is within range
        t = max(0, min(100, t))

        # Find the two stops to interpolate between
        for i in range(len(color_stops) - 1):
            pos1, color1 = color_stops[i]
            pos2, color2 = color_stops[i+1]

            if pos1 <= t <= pos2:
                # Linear interpolation between the two color stops
                ratio = (t - pos1) / (pos2 - pos1)
                r = int(color1[0] + ratio * (color2[0] - color1[0]))
                g = int(color1[1] + ratio * (color2[1] - color1[1]))
                b = int(color1[2] + ratio * (color2[2] - color1[2]))
                return r, g, b

        # Should never reach here
        return 0, 0, 0

    for start_idx in range(0, len(data_array), batch_size):
        end_idx = min(start_idx + batch_size, len(data_array))
        batch_slice = slice(start_idx, end_idx)

        for i in range(end_idx - start_idx):
            # Get data for this record
            humidity = float(data_array[batch_slice][i, 2])

            # Map humidity from min_val-max_val to 0-100 for our color function
            normalized_temp = 100.0 * (humidity - min_val) / (max_val - min_val) if max_val > min_val else 0
            normalized_temp = max(0, min(100, normalized_temp))  # Clamp to 0-100

            alarm_level = 0
            # If we have an alarm_level column (index 3), use it
            if data_array.shape[1] > 3:
                alarm_level = data_array[batch_slice][i, 3]

            # Calculate base y-coordinate for this device
            base_y = device_indices[batch_slice][i] * n_fields

            # Temperature row (even row)
            if not bw:
                # Get RGB color from our direct mapping function
                r, g, b = get_color(normalized_temp)

                # OpenCV uses BGR ordering, not RGB
                temp_rgb = np.array([b, g, r], dtype=np.uint8)
            else:
                # For B&W mode
                gray_value = int(normalized_temp * 2.55)  # 0-100 to 0-255
                gray_value = max(0, min(255, gray_value))
                temp_rgb = np.array([gray_value, gray_value, gray_value], dtype=np.uint8)

            # Set the humidity color in the even row
            wave_m[base_y, x_coords[batch_slice][i]] = temp_rgb

            # Alarm level row (odd row)
            if alarm_level == 0:
                # Green for normal
                alarm_rgb = np.array([0, 255, 0], dtype=np.uint8) #thisis B,G,R !!!
            elif alarm_level == 1:
                # Yellow for warning
                alarm_rgb = np.array([0, 255, 255], dtype=np.uint8)
            else:  # alarm_level == 2
                # Red for critical
                alarm_rgb = np.array([0, 0, 255], dtype=np.uint8)

            # Set the alarm color in the odd row
            wave_m[base_y + 1, x_coords[batch_slice][i]] = alarm_rgb

    return wave_m

def create_smell_optimized_heatmap(arr_stretched, my_data, bw, fields, device_to_index, base_minute, timezone_st, smell_component_stretch_by, selected_date, y_offset):
    """
    Create a heatmap with the exact blue-cyan-green-yellow-red-violet spectrum
    matching Image 2, with green at position 40
    """
    if len(my_data) < 1:
        return

    minutes = 1440
    devices_c = len(device_to_index)
    sensors_c = len(fields)
    stripes = devices_c * sensors_c #2 for upper maxes, lower mins
    arr_source_template = np.full((stripes, minutes+4), -0.001, dtype=float)
    st = time.time()
    time_from_str, time_to_str = GetLocalTimeForDate(selected_date, timezone_st)
    st = time.time()
    arr_source = fast_fill_smell_array_from_timescale(my_data, time_from_str, device_to_index, arr_source_template, timezone_st)
    arr_source = AddSmellLimits_optimized(arr_source, devices_c, sensors_c, percentile=100)
    scaled_day = CalcExtremes(arr_source, minutes, stripes)

    arr_stretched = FillSmellImage_optimized(scaled_day, arr_stretched, y_offset)

    return


def create_optimized_heatmap_simple(my_data, bw, fields, wave_m, device_to_index, base_minute, timezone_st, min_val, max_val):

    if len(my_data) < 1:
        return []


    local_tz = pytz.timezone(timezone_st)

    n_fields = len(fields)
    # Convert my_data to numpy array for faster processing
    data_array = np.array(my_data)

    # Get unique device IDs and create mapping

    # Convert device IDs to indices using vectorized operation
    device_indices = np.vectorize(device_to_index.get)(data_array[:, 1])

    # Calculate x coordinates (minutes from base)
    #minute is in local time zone, and base_minute is UTC
    base_minute_local =  base_minute #.astimezone(local_tz)
    #x_coords = np.array([(minute.replace(tzinfo=datetime.timezone.utc) - base_minute_local).total_seconds()/60 for minute in data_array[:, 0]], dtype=np.int32)
    x_coords = np.array([(minute - base_minute_local).total_seconds()/60 for minute in data_array[:, 0]], dtype=np.int32)

    # Extract values and convert to float
    values = data_array[:, 2:].astype(np.float32)

    if bw:
        # Process in batches to avoid memory issues
        batch_size = 1000
        for start_idx in range(0, len(data_array), batch_size):
            end_idx = min(start_idx + batch_size, len(data_array))
            batch_slice = slice(start_idx, end_idx)


            # Calculate gray values
            gray_values = (values[batch_slice, :] - min_val / (max_val - min_val)) * 255.0

            # Clip values to valid range
            gray_values = np.clip(gray_values, 0, 255).astype(np.uint8)

            # Create y coordinates for each record
            y_coords = (device_indices[batch_slice] * n_fields).reshape(-1, 1) + np.arange(n_fields)

            # Assign values to the image array
            for i in range(end_idx - start_idx):
                wave_m[y_coords[i], x_coords[batch_slice][i]] = gray_values[i, :, np.newaxis]

    else:  # Color mode
        # Process in batches
        batch_size = 1000
        for i in range(0, len(data_array)):
            rgb_value = ((values[i] - min_val) / (max_val - min_val)) * 1279.0
            wave_m[i, x_coords[i]] = rgb_value

    return wave_m


def create_radar_optimized_heatmap(my_data, bw, fields, wave_m, device_to_index, base_minute, timezone_st):

    if len(my_data) < 1:
        return []


    local_tz = pytz.timezone(timezone_st)

    n_fields = len(fields)
    # Convert my_data to numpy array for faster processing
    data_array = np.array(my_data)

    # Get unique device IDs and create mapping

    # Convert device IDs to indices using vectorized operation
    device_indices = np.vectorize(device_to_index.get)(data_array[:, 1])

    # Calculate x coordinates (minutes from base)
    #minute is in local time zone, and base_minute is UTC
    base_minute_local =  base_minute #.astimezone(local_tz)
    #x_coords = np.array([(minute.replace(tzinfo=datetime.timezone.utc) - base_minute_local).total_seconds()/60 for minute in data_array[:, 0]], dtype=np.int32)
    x_coords = np.array([(minute - base_minute_local).total_seconds()/60 for minute in data_array[:, 0]], dtype=np.int32)

    # Extract values and convert to float
    values = data_array[:, 2:].astype(np.float32)

    if bw:
        # Process in batches to avoid memory issues
        batch_size = 1000
        for start_idx in range(0, len(data_array), batch_size):
            end_idx = min(start_idx + batch_size, len(data_array))
            batch_slice = slice(start_idx, end_idx)


            # Calculate gray values
            gray_values = (values[batch_slice, :] / 100.0) * 255.0

            # Clip values to valid range
            gray_values = np.clip(gray_values, 0, 255).astype(np.uint8)

            # Create y coordinates for each record
            y_coords = (device_indices[batch_slice] * n_fields).reshape(-1, 1) + np.arange(n_fields)

            # Assign values to the image array
            for i in range(end_idx - start_idx):
                wave_m[y_coords[i], x_coords[batch_slice][i]] = gray_values[i, :, np.newaxis]

    else:  # Color mode
        # Process in batches
        batch_size = 1000
        for start_idx in range(0, len(data_array), batch_size):
            end_idx = min(start_idx + batch_size, len(data_array))
            batch_slice = slice(start_idx, end_idx)

            # Calculate color values
            color_values = np.zeros_like(values[batch_slice])
            color_values[:, :] = (values[batch_slice, :] / 100.0) * 1279.0  # other fields

            # Create y coordinates for each record
            y_coords = (device_indices[batch_slice] * n_fields).reshape(-1, 1) + np.arange(n_fields)

            # Convert to RGB colors
            for i in range(end_idx - start_idx):
                rgb_values = vectorized_best_color_numpy(color_values[i])
                wave_m[y_coords[i], x_coords[batch_slice][i]] = rgb_values

    return wave_m

def visualize_gmm_fit(stationary_signal, output_file='gmm_explanation.png'):
    """
    Visualize how GMM separates the stationary signal into components
    """
    # Prepare data
    X = stationary_signal.reshape(-1, 1)

    # Fit GMM
    gmm = GaussianMixture(n_components=2, random_state=42)
    gmm.fit(X)

    # Get parameters
    means = gmm.means_.flatten()
    stds = np.sqrt(gmm.covariances_.flatten())
    weights = gmm.weights_

    # Create histogram of actual data
    plt.figure(figsize=(12, 6))

    # Plot histogram of actual data
    plt.hist(X, bins=50, density=True, alpha=0.6, color='gray',
             label='Actual Signal Distribution')

    # Generate points for GMM curves
    x = np.linspace(X.min(), X.max(), 200)

    # Plot individual components
    for i in range(len(means)):
        plt.plot(x, weights[i] * stats.norm.pdf(x, means[i], stds[i]),
                label=f'Component {i+1}: mean={means[i]:.2f}, std={stds[i]:.2f}')

    # Plot combined GMM
    gmm_curve = np.zeros_like(x)
    for i in range(len(means)):
        gmm_curve += weights[i] * stats.norm.pdf(x, means[i], stds[i])
    plt.plot(x, gmm_curve, 'r--', linewidth=2, label='Combined GMM')

    # Add vertical lines for threshold
    baseline = min(means)
    threshold = baseline + 3 * np.sqrt(gmm.covariances_.flatten()[np.argmin(means)])
    plt.axvline(x=baseline, color='g', linestyle='--', label='Baseline')
    plt.axvline(x=threshold, color='r', linestyle='--', label='Threshold')

    plt.title('Gaussian Mixture Model Components of Stationary Signal')
    plt.xlabel('Signal Value')
    plt.ylabel('Density')
    plt.legend()
    plt.grid(True)

    # Save and close
    plt.savefig(output_file, dpi=300, bbox_inches='tight')
    plt.close()

def process_location_data(location_data):
    """
    Convert raw location data into aligned time series.
    """
    timestamps = np.array([t[0] for t in location_data])
    stationary = np.array([t[1] for t in location_data])
    motion = np.array([t[2] for t in location_data])
    return timestamps, stationary, motion

def detect_presence_for_location(stationary_signal, motion_signal,
                                 motion_threshold=5, gmm_components=2):
    """
    Simplified presence detection for a single location.
    Returns presence mask and parameters.
    """
    # Fit GMM to stationary signal
    gmm = GaussianMixture(n_components=gmm_components, random_state=42)
    X = stationary_signal.reshape(-1, 1)
    gmm.fit(X)

    visualize_gmm_fit(stationary_signal, output_file='gmm_explanation.png')

    # Get baseline and threshold
    baseline = min(gmm.means_)[0]
    components_sorted = sorted(zip(gmm.means_.flatten(), gmm.covariances_.flatten()))
    baseline_std = np.sqrt(components_sorted[0][1])
    threshold = baseline + 3 * baseline_std

    # Detect presence
    presence_mask = (motion_signal > motion_threshold) | (stationary_signal > threshold)

    # Smooth presence detection (15 seconds window = 1.5 samples at 10sec sampling)
    smooth_window = 3
    presence_mask = np.convolve(presence_mask.astype(int),
                                np.ones(smooth_window)/smooth_window,
                               mode='same') > 0.5

    return presence_mask, threshold

def find_current_location(data_sets, start_time, end_time, motion_threshold=10):
    """
    Analyze presence across multiple locations for each minute.

    Parameters:
    -----------
    data_sets : dict
        Dictionary of location_name: data_tuples pairs
    start_time : datetime
        Start time for analysis
    end_time : datetime
        End time for analysis
    motion_threshold : float
        Threshold for significant motion detection

    Returns:
    --------
    dict
        Minute by minute analysis of presence and movement
    """
    # Process each location's data
    location_data = {}
    for location, data in data_sets.items():
        timestamps, stationary, motion = process_location_data(data)
        presence, threshold = detect_presence_for_location(stationary, motion, motion_threshold)
        location_data[location] = {
            'timestamps': timestamps,
            'presence': presence,
            'motion': motion,
            'stationary': stationary,
            'threshold': threshold
        }

    # Create minute-by-minute analysis
    current_time = start_time
    results = []

    while current_time < end_time:
        minute_end = current_time + timedelta(minutes=1)

        # Analysis for current minute
        minute_status = {
            'timestamp': current_time,
            'locations': [],
            'moving_locations': [],
            'presence_values': {},
            'motion_values': {},
            'status': 'nobody_present'
        }

        # First pass: collect all presence and motion values
        for location, data in location_data.items():
            # Find indices for current minute
            mask = (data['timestamps'] >= current_time) & (data['timestamps'] < minute_end)
            if not any(mask):
                continue

            presence_in_minute = data['presence'][mask]
            motion_in_minute = data['motion'][mask]
            stationary_in_minute = data['stationary'][mask]

            if any(presence_in_minute):
                minute_status['presence_values'][location] = np.max(stationary_in_minute)
                minute_status['motion_values'][location] = np.max(motion_in_minute)

        # If no presence detected anywhere
        if not minute_status['presence_values']:
            minute_status['status'] = 'nobody_present'
            results.append(minute_status)
            current_time += timedelta(minutes=1)
            continue


        # Find location with strongest presence
        primary_location = max(minute_status['presence_values'].items(),
                             key=lambda x: x[1])[0]

        # Count locations with significant motion
        moving_locations = [loc for loc, motion in minute_status['motion_values'].items()
                          if motion > motion_threshold]

        plot(motion, filename=f"motion.png", title=f"Motion", style='line')
        # Update status based on motion and presence
        if len(moving_locations) > 1:
            # Multiple locations with significant motion indicates multiple people
            minute_status['status'] = 'multiple_people_moving'
            minute_status['locations'] = moving_locations
            minute_status['moving_locations'] = moving_locations
        else:
            # Single or no motion - assign to location with strongest presence
            minute_status['locations'] = [primary_location]
            if moving_locations:
                minute_status['status'] = f'single_person_moving_in_{primary_location}'
                minute_status['moving_locations'] = moving_locations
            else:
                minute_status['status'] = f'single_person_stationary_in_{primary_location}'

        results.append(minute_status)
        current_time += timedelta(minutes=1)

    return results

def get_size(obj, seen=None):
    # Recursively find size of objects and their contents
    if seen is None:
        seen = set()

    obj_id = id(obj)
    if obj_id in seen:
        return 0

    seen.add(obj_id)
    size = sys.getsizeof(obj)

    if isinstance(obj, (list, tuple, set, dict)):
        if isinstance(obj, (list, tuple, set)):
            size += sum(get_size(i, seen) for i in obj)
        else:  # dict
            size += sum(get_size(k, seen) + get_size(v, seen) for k, v in obj.items())

    return size

def CreatePresenceMap(location_image_file, devices_list, selected_date,
                      map_type, force_recreate, chart_type, bw, motion, scale_global,
                      fast, filter_minutes, time_zone_s):

    #global Id2MACDict
    data_sets = {}
    ids_list = []
    time_from_str, time_to_str = GetLocalTimeForDate(selected_date, time_zone_s)
    time_from, time_to = GetLocalTimeForDateSimple(selected_date, time_zone_s)

    for details in devices_list:

        sql = get_device_radar_only_query(str(details[1]), time_from_str, time_to_str, [details[1]])
        print(sql)

        with get_db_connection() as conn:
            with conn.cursor() as cur:
                cur.execute(sql)
                data_sets[details[2]] = cur.fetchall()#cur.fetchone()




    # Get minute-by-minute analysis
    location_analysis = find_current_location(data_sets, time_from, time_to)

    # Example of printing results
    for minute in location_analysis:
        print(f"Time: {minute['timestamp']}")
        print(f"Status: {minute['status']}")
        print(f"Present in: {', '.join(minute['locations'])}")
        if minute['moving_locations']:
            print(f"Movement in: {', '.join(minute['moving_locations'])}")
        print("---")

    print(f"Dictionary  size: {get_size(data_sets)} bytes")
    devices_list_str = ','.join(str(device[1]) for device in devices_list)
    time_from_str, time_to_str = GetLocalTimeForDate(selected_date, time_zone_s)

    sql = get_device_radar_only_query(devices_list_str, time_from_str, time_to_str, ids_list)
    print(sql)

    with get_db_connection() as conn:
        with conn.cursor() as cur:
            cur.execute(sql)
            my_data = cur.fetchall()#cur.fetchone()
            #print(result)
            if my_data == None:
                return False


    #thresholds_dict = {}

    #stretch_to_min_max = True
    #devices_c = len(devices_list)

    #data_sets = {
        #'living_room': my_data1,
        #'kitchen': my_data2,
        #'bedroom1': my_data3,
        #'bedroom2': my_data4,
        #'hallway': my_data5,
        #'bathroom': my_data6,
        #'office': my_data7
    #}

    sensors_c = 1#len(sensors_table)


    image_file = location_image_file

    minutes = 1440

    #search_pattern = os.path.join(scriptDir, "scratch/*_"+str(current_date.year)+"_"+str(current_date.month).rjust(2, '0')+"_"+str(current_date.day)+"_*.pkl")

    #allFiles = [os.path.join(dest_path, f) for f in glob.glob(search_pattern)]
    #rekreate .pckl files if missing
    today_date = datetime.datetime.fromtimestamp(time.time())

    fields = ['m0_max', 'm1_max', 'm2_max', 'm3_max', 'm4_max', 'm5_max',
              'm6_max', 'm7_max', 'm8_max', 'm08_max', 's2_max', 's3_max',
              's4_max', 's5_max', 's6_max', 's7_max', 's8_max', 's28_max', 's28_min'] #Why 'm8_max' and 'm08_max' ?because m08 is m0 + m1 .. to 8!

    fields_n = len(fields)

    stripes = len(devices_list) * fields_n

    #device_counter = 0
    stretch_by = 5

    #arr_source = np.zeros((stripes, minutes), dtype=float)
    arr_stretched = np.zeros((int(stripes*stretch_by), minutes, 3), dtype=np.uint8)  #array to be written as image 3 for RGB channels


    ids_list = []

    labels = []
    label_font = cv2.FONT_HERSHEY_SIMPLEX
    label_font_scale = 1
    label_font_color = (255, 255, 255)
    label_font_thickness = 2
    label_font_line = cv2.LINE_AA
    cnt = 0
    for details in devices_list:
        dev_id =  details[0]
        ids_list.append(details[1])
        descriptor = details[2]
        if details[3] != None and details[3] != "":
            descriptor =  descriptor + " " + details[3]
        if details[6] != None and details[6] != "":
            descriptor =  descriptor + " " + details[6]

        text_dimensions = get_text_dimensions(descriptor, label_font, label_font_scale, label_font_thickness)
        text_height = text_dimensions["height"]
        labels.append((descriptor, (10, 10 + text_height + (cnt)*fields_n*stretch_by), label_font, label_font_scale, label_font_color, label_font_thickness, label_font_line))
        cnt += 1

    sql = get_deployment_radar_only_detailed_query(devices_list_str, time_from_str, time_to_str, ids_list)
    print(sql)
    with get_db_connection() as conn:
        with conn.cursor() as cur:
            cur.execute(sql)
            my_data = cur.fetchall()#cur.fetchone()
            #print(result)
            if my_data == None:
                return False




    # Get start and end times from your data
    start_time = min(data_sets['living_room'][0][0],
                    data_sets['kitchen'][0][0],
                    # ... add other locations
                    )
    end_time = max(data_sets['living_room'][-1][0],
                   data_sets['kitchen'][-1][0],
                   # ... add other locations
                   )

    # Get minute-by-minute analysis
    location_analysis = find_current_location(data_sets, start_time, end_time)

    # Example of printing results
    for minute in location_analysis:
        print(f"Time: {minute['timestamp']}")
        print(f"Status: {minute['status']}")
        print(f"Present in: {', '.join(minute['locations'])}")
        if minute['moving_locations']:
            print(f"Movement in: {', '.join(minute['moving_locations'])}")
        print("---")
#----------------------------------------------------------------------------------------------------



        print(sql)
        with get_db_connection() as conn:
            with conn.cursor() as cur:
                cur.execute(sql)
                my_data = cur.fetchall()#cur.fetchone()
                #print(result)
                if my_data == None:
                    return False


        #device_ids = sorted(set(record[1] for record in my_data))
        device_to_index = {device: idx for idx, device in enumerate(ids_list)}

        base_minute = ConvertToBase(time_from_str, time_zone_s)
        st = time.time()
        if True:
            wave_m = np.zeros((stripes, 1440, 3), dtype=np.uint8)
            wave_m = create_radar_optimized_heatmap(my_data, bw, fields, wave_m, device_to_index, base_minute, time_zone_s)
            print(time.time()-st)

        if False:
            #base_minute = my_data[0][0]# min(record[0] for record in my_data)

            wave_m = np.zeros((stripes, 1440, 3), dtype=np.uint8)

            for record in my_data:

                #(minute,device_id,absent_min,stationary_max,moving_max,both_max,m0_max,m1_max,m2_max,m3_max,m4_max,
                # m5_max,m6_max,m7_max,m8_max,m08_max,s2_max,s3_max,s4_max,s5_max,s6_max,s7_max,s8_max,s28_max) = record
                minute, device_id = record[0:2]
                values = record[2:]  # All the max/min values
                x = int((minute - base_minute).total_seconds()/60)
                device_idx = device_to_index[device_id]

                if bw:
                    for field_idx, value in enumerate(values):
                        # Calculate y position
                        y = device_idx * fields_n + field_idx

                        # Convert value to grayscale (0-100 to 0-255)
                        gray_value = int((value / 100.0) * 255.0)

                        # Set RGB values (all same for grayscale)
                        wave_m[y, x] = [gray_value, gray_value, gray_value]
                else: #color
                    for field_idx, value in enumerate(values):
                        # Calculate y position
                        y = device_idx * 22 + field_idx

                        # Convert value to grayscale (0-100 to 0-255)
                        gray_value = int((value / 100.0) * 1279.0)

                        # Set RGB values (all same for grayscale)
                        wave_m[y, x] = BestColor(gray_value)

            print(time.time()-st)

        st = time.time()
        for yy in range(stripes):
            rgb_row = wave_m[yy]
            for stretch_index in range(stretch_by):
                y = yy * stretch_by + stretch_index
                arr_stretched[y, :] = rgb_row

        print(time.time()-st)

        SaveImageInBlob(image_file, arr_stretched, labels)
        #arr_source[2*gate, :] = wave_m
        #rgb_row = np.zeros(( 1440, 3), dtype=np.uint8)

        #for col in range(1440):
            #sens_val = wave_m[col]
            #if sens_val != 0:
                #r,g,b=BestColor(km*(sens_val-m_min))
                #if r > 255 or g > 255 or b > 255:
                    #print(r,g,b)
                #rgb_row[col] = r,g,b

        #for stretch_index in range(stretch_by):
            #y = device_counter * (18*stretch_by) + 2*gate * stretch_by + stretch_index
            ##print(y, row, devices_c, sensor_index, location_index, stretch_index)
            ##arr_stretched[y, :] = rgb_row

        #if gate > 1:
            #ks = 0
            #if(s_max > s_min):
                #if bw:
                    #ks = 255/(s_max - s_min)
                #else:
                    #ks = 1280/(s_max - s_min)
            ##wave_m = np.array([km*(item[0]-m_min) for item in minute_radar_lists[:1440][gate]])
            #wave_s = np.array([0.0] * 1440)

            #for minute_m in range(1440):
                #wave_s[minute_m] = minute_radar_lists[minute_m][gate+7]
            ##wave_m = np.array([item[0] for item in minute_radar_lists[:1440][gate]])
            ##DoDisplay2(wave_m, wave_s, location_name+" "+str(dev_id)+" "+ description+" "+ str(gate))
            #arr_source[2*gate + 1, :] = wave_s

            #rgb_row = np.zeros(( 1440, 3), dtype=np.uint8)

            #for col in range(1440):
                #sens_val = wave_s[col]
                #if sens_val != 0:
                    #if bw:
                        #r = ks*(sens_val-s_min)
                        #g = r
                        #b = r
                    #else:
                        #r,g,b=BestColor(ks*(sens_val-s_min))
                    ##print(r,g,b)
                    #rgb_row[col] = r,g,b

            #for stretch_index in range(stretch_by):
                #y = device_counter * (18*stretch_by) + (2*(gate) + 1) * stretch_by + stretch_index
                #arr_stretched[y, :] = rgb_row
                #y = device_counter * (18*stretch_by) + (2*(gate)) * stretch_by + stretch_index
                #arr_stretched[y, :] = rgb_row

        print("stop")

def ConvertToBase(time_from_str, time_zone_s):
    print(time_from_str)
    dt = datetime.datetime.strptime(time_from_str, "%Y-%m-%d %H:%M:%S%z")
    return dt

def CreateFullLocationMap(location_image_file, devices_list, selected_date,
                          map_type, force_recreate, chart_type, bw, motion, scale_global, fast, filter_minutes, time_zone_s):
    #global Id2MACDict

    thresholds_dict = {}

    stretch_to_min_max = True
    devices_c = len(devices_list)

    if devices_c == 0:
        return

    sensors_c = 1#len(sensors_table)
    time_from_str, time_to_str = GetLocalTimeForDate(selected_date, time_zone_s)

    image_file = location_image_file

    minutes = 1440


    #search_pattern = os.path.join(scriptDir, "scratch/*_"+str(current_date.year)+"_"+str(current_date.month).rjust(2, '0')+"_"+str(current_date.day)+"_*.pkl")

    #allFiles = [os.path.join(dest_path, f) for f in glob.glob(search_pattern)]
    #rekreate .pckl files if missing
    today_date = datetime.datetime.fromtimestamp(time.time())

    if scale_global and chart_type != 3 and chart_type != 4: #"digital" and chart_type != "collapsed"

        max_gate={}
        for gate in range(9):
            max_gate[str(gate)+"_m"] = 0
            max_gate[str(gate)+"_s"] = 0

        device_counter = 0
        for details in devices_list:
            MAC, threshold, location_name, description = details
            if threshold == None:
                threshold = '["s3_max",12]'


            #day_minutes_data = [(0,0)] * (24 * 60 + 2)
            #day_minutes_data_l = [[0] * 10 for _ in range(24 * 60 + 2)]

            minute_radar_lists = ReadDailyRadar(MAC, current_date)

            for gate in range(9):
                for minute_m in range(1440):
                    if (minute_radar_lists[minute_m][gate] > max_gate[str(gate)+"_m"]):
                        max_gate[str(gate)+"_m"] = minute_radar_lists[minute_m][gate]

                    if gate > 1:
                        if (minute_radar_lists[minute_m][gate + 7] > max_gate[str(gate)+"_s"]):
                            max_gate[str(gate)+"_s"] = minute_radar_lists[minute_m][gate + 7]


    if (chart_type == 2):  #"analog"


        #fields = ['absent_min', 'stationary_max', 'moving_max', 'both_max',
                  #'m0_max', 'm1_max', 'm2_max', 'm3_max', 'm4_max', 'm5_max',
                  #'m6_max', 'm7_max', 'm8_max', 'm08_max', 's2_max', 's3_max',
                  #'s4_max', 's5_max', 's6_max', 's7_max', 's8_max', 's28_max']
        fields = ['m0_max', 'm1_max', 'm2_max', 'm3_max', 'm4_max', 'm5_max',
                  'm6_max', 'm7_max', 'm8_max', 'm08_max', 's2_max', 's3_max',
                  's4_max', 's5_max', 's6_max', 's7_max', 's8_max', 's28_max', 's28_min']


        fields_n = len(fields)

        stripes = len(devices_list) * fields_n

        device_counter = 0
        stretch_by = 5

        arr_source = np.zeros((stripes, minutes), dtype=float)
        arr_stretched = np.zeros((int(stripes*stretch_by), minutes, 3), dtype=np.uint8)  #array to be written as image 3 for RGB channels



        devices_list_str = ','.join(str(device[1]) for device in devices_list)
        ids_list = []

        labels = []
        label_font = cv2.FONT_HERSHEY_SIMPLEX
        label_font_scale = 1
        label_font_color = (255, 255, 255)
        label_font_thickness = 2
        label_font_line = cv2.LINE_AA
        cnt = 0
        for details in devices_list:
            dev_id =  details[0]
            ids_list.append(details[1])
            descriptor = details[2]
            if details[3] != None and details[3] != "":
                descriptor =  descriptor + " " + details[3]
            if details[6] != None and details[6] != "":
                descriptor =  descriptor + " " + details[6]

            text_dimensions = get_text_dimensions(descriptor, label_font, label_font_scale, label_font_thickness)
            text_height = text_dimensions["height"]
            labels.append((descriptor, (10, 10 + text_height + (cnt)*fields_n*stretch_by), label_font, label_font_scale, label_font_color, label_font_thickness, label_font_line))
            cnt += 1
        sql = get_deployment_radar_only_detailed_query(devices_list_str, time_from_str, time_to_str, ids_list)
        print(sql)
        with get_db_connection() as conn:
            with conn.cursor() as cur:
                cur.execute(sql)
                my_data = cur.fetchall()#cur.fetchone()
                #print(result)
                if my_data == None:
                    return False


        #device_ids = sorted(set(record[1] for record in my_data))
        device_to_index = {device: idx for idx, device in enumerate(ids_list)}
        # Calculate base minute
        base_minute = ConvertToBase(time_from_str, time_zone_s)

        st = time.time()
        if True:
            wave_m = np.zeros((stripes, 1440, 3), dtype=np.uint8)
            wave_m = create_radar_optimized_heatmap(my_data, bw, fields, wave_m, device_to_index, base_minute, time_zone_s)
            print(time.time()-st)

        if False:
            #base_minute = my_data[0][0]# min(record[0] for record in my_data)

            wave_m = np.zeros((stripes, 1440, 3), dtype=np.uint8)

            for record in my_data:

                #(minute,device_id,absent_min,stationary_max,moving_max,both_max,m0_max,m1_max,m2_max,m3_max,m4_max,
                # m5_max,m6_max,m7_max,m8_max,m08_max,s2_max,s3_max,s4_max,s5_max,s6_max,s7_max,s8_max,s28_max) = record
                minute, device_id = record[0:2]
                values = record[2:]  # All the max/min values
                x = int((minute - base_minute).total_seconds()/60)
                device_idx = device_to_index[device_id]

                if bw:
                    for field_idx, value in enumerate(values):
                        # Calculate y position
                        y = device_idx * fields_n + field_idx

                        # Convert value to grayscale (0-100 to 0-255)
                        gray_value = int((value / 100.0) * 255.0)

                        # Set RGB values (all same for grayscale)
                        wave_m[y, x] = [gray_value, gray_value, gray_value]
                else: #color
                    for field_idx, value in enumerate(values):
                        # Calculate y position
                        y = device_idx * 22 + field_idx

                        # Convert value to grayscale (0-100 to 0-255)
                        gray_value = int((value / 100.0) * 1279.0)

                        # Set RGB values (all same for grayscale)
                        wave_m[y, x] = BestColor(gray_value)

            print(time.time()-st)

        st = time.time()
        for yy in range(stripes):
            rgb_row = wave_m[yy]
            for stretch_index in range(stretch_by):
                y = yy * stretch_by + stretch_index
                arr_stretched[y, :] = rgb_row

        print(time.time()-st)

        SaveImageInBlob(image_file, arr_stretched, labels)
        #arr_source[2*gate, :] = wave_m
        #rgb_row = np.zeros(( 1440, 3), dtype=np.uint8)

        #for col in range(1440):
            #sens_val = wave_m[col]
            #if sens_val != 0:
                #r,g,b=BestColor(km*(sens_val-m_min))
                #if r > 255 or g > 255 or b > 255:
                    #print(r,g,b)
                #rgb_row[col] = r,g,b

        #for stretch_index in range(stretch_by):
            #y = device_counter * (18*stretch_by) + 2*gate * stretch_by + stretch_index
            ##print(y, row, devices_c, sensor_index, location_index, stretch_index)
            ##arr_stretched[y, :] = rgb_row

        #if gate > 1:
            #ks = 0
            #if(s_max > s_min):
                #if bw:
                    #ks = 255/(s_max - s_min)
                #else:
                    #ks = 1280/(s_max - s_min)
            ##wave_m = np.array([km*(item[0]-m_min) for item in minute_radar_lists[:1440][gate]])
            #wave_s = np.array([0.0] * 1440)

            #for minute_m in range(1440):
                #wave_s[minute_m] = minute_radar_lists[minute_m][gate+7]
            ##wave_m = np.array([item[0] for item in minute_radar_lists[:1440][gate]])
            ##DoDisplay2(wave_m, wave_s, location_name+" "+str(dev_id)+" "+ description+" "+ str(gate))
            #arr_source[2*gate + 1, :] = wave_s

            #rgb_row = np.zeros(( 1440, 3), dtype=np.uint8)

            #for col in range(1440):
                #sens_val = wave_s[col]
                #if sens_val != 0:
                    #if bw:
                        #r = ks*(sens_val-s_min)
                        #g = r
                        #b = r
                    #else:
                        #r,g,b=BestColor(ks*(sens_val-s_min))
                    ##print(r,g,b)
                    #rgb_row[col] = r,g,b

            #for stretch_index in range(stretch_by):
                #y = device_counter * (18*stretch_by) + (2*(gate) + 1) * stretch_by + stretch_index
                #arr_stretched[y, :] = rgb_row
                #y = device_counter * (18*stretch_by) + (2*(gate)) * stretch_by + stretch_index
                #arr_stretched[y, :] = rgb_row

        print("stop")
    elif (chart_type == 3): #"digital"
        device_counter = 0
        for details in devices_list:
            dev_id = details[0]
            MAC, threshold, location_id, description = GetMacThrFromId(dev_id)
            if threshold == None:
                threshold = 30

            sensor = "Radar"
            location_name = location_names[location_id]
            pickle_file = os.path.join(scriptDir, "scratch/"+MAC.upper() +"_"+str(current_date.year)+"_"+str(current_date.month).rjust(2, '0')+"_"+str(current_date.day)+"_radarM.pkl")
            pickle_file = pickle_file.replace("\\","/")
            #day_minutes_data = [(0,0)] * (24 * 60 + 2)
            #day_minutes_data_l = [[0] * 10 for _ in range(24 * 60 + 2)]

            minute_radar_lists = ReadDailyRadar(MAC, current_date)

            y = 0
            sensor_index = 0

            #location_index = 0

            for gate in range(9):
                threshold = 15
                if (gate > 1):
                    threshold = thresholds_dict[dev_id][gate-2]

                for minute_m in range(1440):
                    if (minute_radar_lists[minute_m][gate] > threshold):
                        minute_radar_lists[minute_m][gate] = 100
                    else:
                        minute_radar_lists[minute_m][gate] = 0

                    if gate > 1:
                        if (minute_radar_lists[minute_m][gate + 7] > threshold):
                            minute_radar_lists[minute_m][gate + 7] = 100
                        else:
                            minute_radar_lists[minute_m][gate + 7] = 0

                m_max = 100
                m_min = 0
                s_max = 100
                s_min = 0

                km = 0
                if(m_max > m_min):
                    km = 1280/(m_max - m_min)
                #wave_m = np.array([km*(item[0]-m_min) for item in minute_radar_lists[:1440][gate]])
                wave_m = np.array([0.0] * 1440)

                for minute_m in range(1440):
                    wave_m[minute_m] = minute_radar_lists[minute_m][gate]

                if gate < 2:
                    DoDisplay(wave_m, location_name+" "+ description+" " + str(gate))
                #wave_m = np.array([item[0] for item in minute_radar_lists[:1440][gate]])
                arr_source[2*gate, :] = wave_m
                rgb_row = np.zeros(( 1440, 3), dtype=np.uint8)

                for col in range(1440):
                    sens_val = wave_m[col]
                    if sens_val != 0:
                        r,g,b=BestColor(km*(sens_val-m_min))
                        #print(r,g,b)
                        rgb_row[col] = r,g,b

                for stretch_index in range(stretch_by):
                    y = device_counter * (18*stretch_by) + 2*gate * stretch_by + stretch_index
                    #print(y, row, devices_c, sensor_index, location_index, stretch_index)

                    #arr_stretched[y, :] = rgb_row

                if gate > 1:
                    ks = 0
                    if(s_max > s_min):
                        if bw:
                            ks = 255/(s_max - s_min)
                        else:
                            ks = 1280/(s_max - s_min)
                    #wave_m = np.array([km*(item[0]-m_min) for item in minute_radar_lists[:1440][gate]])
                    wave_s = np.array([0.0] * 1440)

                    for minute_m in range(1440):
                        wave_s[minute_m] = minute_radar_lists[minute_m][gate+7]
                    #wave_m = np.array([item[0] for item in minute_radar_lists[:1440][gate]])
                    DoDisplay2(wave_m, wave_s, location_name+" "+str(dev_id)+" "+ description+" "+ str(gate))
                    arr_source[2*gate + 1, :] = wave_s

                    rgb_row = np.zeros(( 1440, 3), dtype=np.uint8)

                    for col in range(1440):
                        sens_val = wave_s[col]
                        if sens_val != 0:
                            if bw:
                                r = ks*(sens_val-s_min)
                                g = r
                                b = r
                            else:
                                r,g,b=BestColor(ks*(sens_val-s_min))
                            #print(r,g,b)
                            rgb_row[col] = r,g,b

                    for stretch_index in range(stretch_by):
                        y = device_counter * (18*stretch_by) + (2*(gate) + 1) * stretch_by + stretch_index
                        arr_stretched[y, :] = rgb_row
                        y = device_counter * (18*stretch_by) + (2*(gate)) * stretch_by + stretch_index
                        arr_stretched[y, :] = rgb_row

            device_counter += 1
            print("stop")

    elif (chart_type == 4): #"collapsed"

        stretch_by = 50
        arr_source = np.zeros((1, minutes), dtype=float)
        arr_stretched = np.zeros((int(stretch_by), minutes, 3), dtype=np.uint8)  #array to be written as image 3 for RGB channels


        device_counter = 0
        wave_m = [["", -1] for _ in range(1440)]
        devices_list_str = ','.join(str(device[1]) for device in devices_list)
        ids_list = []
        radar_fields_of_interest = []
        for details in devices_list:
            threshold_str = details[5]
            try:
                threshold_lst = json.loads(threshold_str)
            except:
                threshold_lst = ["s3_max",12]

            radar_field = threshold_lst[0]
            if radar_field not in radar_fields_of_interest:
                radar_fields_of_interest.append(radar_field)

            threshold = threshold_lst[1]
            dev_id =  details[0]
            ids_list.append(details[1])

        sql = get_deployment_radar_only_colapsed_query(devices_list_str, time_from_str, time_to_str, ids_list, radar_fields_of_interest)
        print(sql)
        with get_db_connection() as conn:
            with conn.cursor() as cur:
                cur.execute(sql)
                my_data = cur.fetchall()#cur.fetchone()
                #print(result)
                if my_data == None:
                    return False

        device_id_2_threshold = {}
        device_id_2_location = {0: "Outside"}
        row_nr_2_device_id = {}
        cnt = 0
        row_nr_2_device_id[0] = 0
        for details in devices_list:

            well_id, device_id, location_name, description, MAC, radar_threshold_group_st, close_to = details #(266, 559, 'Bathroom', None, '64B70888FAB0', '["s3_max",12]')

            cnt += 1
            row_nr_2_device_id[cnt] = device_id

            if radar_threshold_group_st == None:
                radar_threshold_group_st = '["s3_max",12]' #last value is threshold to s28 composite

            if len(radar_threshold_group_st) > 8:
                radar_threshold_group = json.loads(radar_threshold_group_st)
            else:
                radar_threshold_group = ["s3_max",12]

            device_id_2_location[device_id] = location_name

            device_id_2_threshold[device_id] = radar_threshold_group

        target_tz = pytz.timezone(time_zone_s)
        st = time.time()

        #each record in my_data has time, device_id and radar_fields_of_interest in it
        result_np = None
        try:
            result_np = process_wave_data_numpy(image_file, my_data, time_zone_s, device_id_2_threshold, radar_fields_of_interest)
            print(time.time() - st)
        except Exception as err:
            print(str(err))

        if False:
            for record in my_data:
                time_val, device_id, min_val, max_val = record
                radar_threshold = device_id_2_threshold[device_id]
                local_time = time_val.astimezone(target_tz)
                minute_m = int((local_time - local_time.replace(hour=0, minute=0, second=0, microsecond=0)).total_seconds() / 60)

                if (wave_m[minute_m][0] == ""):
                    if max_val > radar_threshold:
                        wave_m[minute_m][0] = device_id
                        wave_m[minute_m][1] = max_val
                else:
                    if max_val > radar_threshold:
                        if max_val > wave_m[minute_m][1]:
                            wave_m[minute_m][0] = device_id
                            wave_m[minute_m][1] = max_val



        print(time.time()-st)
        if result_np is not None:
            wave_m = result_np
            rgb_row = np.zeros(( 1440, 3), dtype=np.uint8)
            #wave_m = FilterGlitches(wave_m, filter_minutes)
            r = 0
            g = 0
            b = 0

            if isinstance(wave_m[0], np.int64):
                inital_device_id = row_nr_2_device_id[wave_m[0]]
            else:
                inital_device_id = 0

            present_at = [[inital_device_id, 0, 1]] #device_id, minute, duration

            for minute_m in range(1440):
                try:

                    if isinstance(wave_m[minute_m], np.int64):
                        device_id = row_nr_2_device_id[wave_m[minute_m]]
                    else:
                        device_id = 0

                    if device_id != "" and device_id != -1:
                        r,g,b = Loc2Color[device_id_2_location[device_id]][0]
                        rgb_row[minute_m] = b,g,r

                    if minute_m > 0:
                        if present_at[-1][0] != device_id:
                            present_at.append([device_id, minute_m, 1])
                        else:
                            present_at[-1][2] += 1

                except Exception as err:
                    print(str(err))

            for stretch_index in range(stretch_by):
                y = stretch_index
                arr_stretched[y, :] = rgb_row

            #print("stop")
            #print(r,g,b)
            SaveObjectInBlob(image_file+".bin", present_at)
            SaveImageInBlob(image_file, arr_stretched)

def CreateFullLocationMapLabelsOut(location_image_file, devices_list, selected_date,
                          map_type, force_recreate, chart_type, bw, motion, scale_global, fast, filter_minutes, time_zone_s):
    #global Id2MACDict

    thresholds_dict = {}

    stretch_to_min_max = True
    devices_c = len(devices_list)

    if devices_c == 0:
        return

    sensors_c = 1#len(sensors_table)
    time_from_str, time_to_str = GetLocalTimeForDate(selected_date, time_zone_s)

    image_file = location_image_file

    minutes = 1440


    #search_pattern = os.path.join(scriptDir, "scratch/*_"+str(current_date.year)+"_"+str(current_date.month).rjust(2, '0')+"_"+str(current_date.day)+"_*.pkl")

    #allFiles = [os.path.join(dest_path, f) for f in glob.glob(search_pattern)]
    #rekreate .pckl files if missing
    today_date = datetime.datetime.fromtimestamp(time.time())


    if (chart_type == 8):  #"all graphs"

        fields = ['m0_max', 'm1_max', 'm2_max', 'm3_max', 'm4_max', 'm5_max',
                  'm6_max', 'm7_max', 'm8_max', 'm08_max', 's2_max', 's3_max',
                  's4_max', 's5_max', 's6_max', 's7_max', 's8_max', 's28_max', 's28_min']


        show_radar = True
        show_light = True
        show_temperature = True
        show_humidity = True
        show_smell = True

        labels_width = 200
        title_labels_height = 40
        title_label_width = 100

        #common
        label_font = cv2.FONT_HERSHEY_SIMPLEX
        label_font_line = cv2.LINE_AA

        #different
        title_label_font_scale = 1
        title_label_font_color =  (0, 0, 0)#(128, 255, 255)
        title_label_font_thickness = 2

        label_font_scale = 0.5
        label_font_color = (0, 0, 0)#(0, 255, 255)
        label_font_thickness = 1

        fields_n = len(fields)

        radar_stripes = len(devices_list) * fields_n
        radar_stretch_by = 5

        light_stripes = len(devices_list)
        light_stretch_by = 20

        smell_sensors_stripes = 10 * len(devices_list)
        other_sensors_stripes = len(devices_list)

        temp_stripe_width = 15
        alarm_stripe_width = 5
        temperature_stretch_by = temp_stripe_width + alarm_stripe_width  # Total height per device

        humidity_stripe_width = 15
        humidity_stretch_by = humidity_stripe_width + alarm_stripe_width
        smell_component_stretch_by = 20

        text_dimensions = get_text_dimensions("TEST", label_font, label_font_scale, label_font_thickness)
        text_height = text_dimensions["height"]

        all_maps_height = 0

        #  radar, light, temperature, humidity, smell*10

        if show_radar:
            all_maps_height = title_labels_height + radar_stripes*radar_stretch_by

        if show_light:
            all_maps_height = all_maps_height + title_labels_height + other_sensors_stripes*light_stretch_by

        if show_temperature:
            all_maps_height = all_maps_height + title_labels_height + other_sensors_stripes*temperature_stretch_by

        if show_humidity:
            all_maps_height = all_maps_height + title_labels_height + other_sensors_stripes*humidity_stretch_by

        if show_smell:
            all_maps_height = all_maps_height + title_labels_height + other_sensors_stripes*smell_component_stretch_by * 10


        if all_maps_height == 0:
            return

        vertical_offset = 0
        arr_stretched = np.full((all_maps_height, minutes+labels_width, 3), [255, 174, 70], dtype=np.uint8)

        #Lets add divider lines
        x = 190
        if show_radar:
            stretch_by = radar_stretch_by
            cnt = 0
            for details in devices_list:
                y = vertical_offset  + title_labels_height + (cnt)*fields_n*stretch_by
                arr_stretched[y, 190:201, :] = 0
                cnt += 1

            section_height = title_labels_height + radar_stripes*radar_stretch_by
            vertical_offset = vertical_offset + section_height
        if show_light:
            stretch_by = light_stretch_by
            cnt = 0

            for details in devices_list:
                y = vertical_offset + title_labels_height+ (cnt)*1*stretch_by
                arr_stretched[y, 190:201, :] = 0
                cnt += 1

            section_height = title_labels_height + other_sensors_stripes*stretch_by
            vertical_offset = vertical_offset + section_height

        if show_temperature:
            stretch_by = temperature_stretch_by
            cnt = 0

            for details in devices_list:
                y = vertical_offset + title_labels_height+ (cnt)*1*stretch_by
                arr_stretched[y, 190:201, :] = 0
                cnt += 1
            section_height = title_labels_height + other_sensors_stripes*stretch_by
            vertical_offset = vertical_offset + section_height
        if show_humidity:
            stretch_by = humidity_stretch_by
            cnt = 0

            for details in devices_list:
                y = vertical_offset + title_labels_height+ (cnt)*1*stretch_by
                arr_stretched[y, 190:201, :] = 0
                cnt += 1
            section_height = title_labels_height + other_sensors_stripes*humidity_stretch_by
            vertical_offset = vertical_offset + section_height
        if show_smell:
            stretch_by = smell_component_stretch_by

            cnt = 0

            for details in devices_list:
                y = vertical_offset + title_labels_height+ (cnt)*10*stretch_by
                arr_stretched[y, 190:201, :] = 0
                cnt += 1
            #section_height = title_labels_height + other_sensors_stripes**stretch_by * 10
            #vertical_offset = vertical_offset + section_height

            #all_maps_height = all_maps_height + title_labels_height + other_sensors_stripes*stretch_by * 10




        devices_list_str = ','.join(str(device[1]) for device in devices_list)
        ids_list = []
        labels = []
        title_labels = []
        vertical_offset = 0
        ######################################## RADAR ##################################################################
        if show_radar:
            title_label_text = "RADAR"
            fields_s = fields
            stripes = radar_stripes
            stretch_by = radar_stretch_by
            title_text_dimensions = get_text_dimensions(title_label_text, label_font, title_label_font_scale, label_font_thickness)
            title_text_height = title_text_dimensions["height"]
            title_label_width = title_text_dimensions["width"]
            title_label = (title_label_text, (int(labels_width + minutes * 0.5 - title_label_width / 2), vertical_offset + 10 + title_text_height), label_font, title_label_font_scale, title_label_font_color, title_label_font_thickness, label_font_line)

            title_labels.append(title_label)

            cnt = 0
            for details in devices_list:
                dev_id =  details[0]
                ids_list.append(details[1])
                descriptor = details[2]
                if details[3] != None and details[3] != "":
                    descriptor =  descriptor + " " + details[3]
                if details[6] != None and details[6] != "":
                    descriptor =  descriptor + " " + details[6]

                text_dimensions = get_text_dimensions(descriptor, label_font, label_font_scale, label_font_thickness)
                text_height = text_dimensions["height"]
                labels.append((descriptor, (10, vertical_offset + title_labels_height+40+text_height + (cnt)*fields_n*stretch_by), label_font, label_font_scale, label_font_color, label_font_thickness, label_font_line))
                cnt += 1
            sql = get_deployment_radar_only_detailed_query(devices_list_str, time_from_str, time_to_str, ids_list)
            print(sql)
            with get_db_connection() as conn:
                with conn.cursor() as cur:
                    cur.execute(sql)
                    my_data = cur.fetchall()#cur.fetchone()
                    #print(result)
                    if my_data != None:

                        device_to_index = {device: idx for idx, device in enumerate(ids_list)}
                        # Calculate base minute
                        base_minute = ConvertToBase(time_from_str, time_zone_s)

                        st = time.time()
                        if True:
                            wave_m = np.zeros((stripes, 1440, 3), dtype=np.uint8)
                            wave_m = create_radar_optimized_heatmap(my_data, bw, fields_s, wave_m, device_to_index, base_minute, time_zone_s)
                            print(time.time()-st)


                        st = time.time()
                        for yy in range(stripes):
                            rgb_row = wave_m[yy]
                            for stretch_index in range(radar_stretch_by):
                                y = yy * radar_stretch_by + stretch_index
                                arr_stretched[title_labels_height+y, 200:] = rgb_row

                        print(time.time()-st)
                        vertical_offset = vertical_offset + title_labels_height + stripes*radar_stretch_by
        ######################################## LIGHT ##################################################################
        if show_light:
            title_label_text = "LIGHT"
            fields_s = ['light']
            min_val = 0
            max_val = 4095
            stretch_by = light_stretch_by
            stripes = len(devices_list) * len(fields_s)  # Calculate number of rows needed
            # Calculate the correct vertical offset for light section

            # Draw the light section title at the correct position
            title_text_dimensions = get_text_dimensions(title_label_text, label_font, title_label_font_scale, label_font_thickness)
            title_text_height = title_text_dimensions["height"]
            title_label_width = title_text_dimensions["width"]
            title_label = (title_label_text, (int(labels_width + minutes * 0.5 - title_label_width / 2),
                          vertical_offset + 10 + title_text_height),
                          label_font, title_label_font_scale, title_label_font_color, title_label_font_thickness, label_font_line)

            title_labels.append(title_label)

            # Draw device labels for light section
            cnt = 0
            light_ids_list = []  # Create a separate list for light section
            for details in devices_list:
                dev_id = details[0]
                light_ids_list.append(details[1])
                descriptor = details[2]
                if details[3] != None and details[3] != "":
                    descriptor = descriptor + " " + details[3]
                if details[6] != None and details[6] != "":
                    descriptor = descriptor + " " + details[6]

                text_dimensions = get_text_dimensions(descriptor, label_font, label_font_scale, label_font_thickness)
                text_height = text_dimensions["height"]
                # Position labels in the light section
                labels.append((descriptor, (10, vertical_offset + title_labels_height + text_height + (cnt)*len(fields_s)*stretch_by),
                              label_font, label_font_scale, label_font_color, label_font_thickness, label_font_line))
                cnt += 1

            # Get light data using the existing query function
            sql = get_deployment_light_only_query(devices_list_str, time_from_str, time_to_str, light_ids_list)
            print(sql)
            with get_db_connection() as conn:
                with conn.cursor() as cur:
                    cur.execute(sql)
                    my_data = cur.fetchall()

                    if my_data != None and len(my_data) > 0:
                        device_to_index = {device: idx for idx, device in enumerate(light_ids_list)}

                        # Calculate base minute
                        base_minute = ConvertToBase(time_from_str, time_zone_s)

                        # Process light data
                        st = time.time()

                        wave_m = np.zeros((stripes, 1440, 3), dtype=np.uint8)

                        # Use the light-specific function
                        wave_m = create_light_optimized_heatmap(my_data, bw, fields_s, wave_m,
                                                              device_to_index, base_minute, time_zone_s,
                                                              min_val, max_val)
                        print(f"Light heatmap creation time: {time.time()-st:.4f} seconds")

                        # Stretch the heatmap vertically
                        st = time.time()
                        section_start = vertical_offset + title_labels_height

                        for yy in range(stripes):
                            rgb_row = wave_m[yy]
                            for stretch_index in range(stretch_by):
                                y = yy * stretch_by + stretch_index
                                target_y = section_start + y

                                # Make sure we're within bounds of the array
                                if target_y < arr_stretched.shape[0]:
                                    arr_stretched[target_y, labels_width:] = rgb_row
                                else:
                                    print(f"Warning: Row {target_y} is out of bounds (max: {arr_stretched.shape[0]-1})")

                        vertical_offset = vertical_offset + title_labels_height + stripes*stretch_by
                        print(f"Light stretching time: {time.time()-st:.4f} seconds")

        ######################################## TEMPERATURE ##################################################################
        if show_temperature:
            title_label_text = "TEMPERATURE"
            fields_s = ['temperature', 'temperature_state']

            # Define different stripe widths for temperature and alarm

            temp_offset = -16.0
            min_val = 20
            max_val = 30

            # Calculate the correct vertical offset for temperature section
            vertical_offset = 0
            if show_radar:
                vertical_offset += title_labels_height + radar_stripes * radar_stretch_by
            if show_light:
                vertical_offset += title_labels_height + other_sensors_stripes * light_stretch_by

            stripes = len(devices_list) * len(fields_s)  # Number of rows needed in data array

            # Draw the temperature section title
            title_text_dimensions = get_text_dimensions(title_label_text, label_font, title_label_font_scale, label_font_thickness)
            title_text_height = title_text_dimensions["height"]
            title_label_width = title_text_dimensions["width"]
            title_label = (title_label_text, (int(labels_width + minutes * 0.5 - title_label_width / 2),
                          vertical_offset + 10 + title_text_height),
                          label_font, title_label_font_scale, title_label_font_color, title_label_font_thickness, label_font_line)

            title_labels.append(title_label)

            # Draw device labels for temperature section
            cnt = 0
            temp_ids_list = []  # Create a separate list for temperature section
            for details in devices_list:
                dev_id = details[0]
                temp_ids_list.append(details[1])
                descriptor = details[2]
                if details[3] != None and details[3] != "":
                    descriptor = descriptor + " " + details[3]
                if details[6] != None and details[6] != "":
                    descriptor = descriptor + " " + details[6]

                text_dimensions = get_text_dimensions(descriptor, label_font, label_font_scale, label_font_thickness)
                text_height = text_dimensions["height"]
                # Position labels in the temperature section
                y_pos = vertical_offset + title_labels_height + text_height + cnt * temperature_stretch_by
                #y_pos = vertical_offset + title_labels_height + text_height + (cnt)*len(fields_s)*stretch_by)
                labels.append((descriptor, (10, y_pos), label_font, label_font_scale,
                             label_font_color, label_font_thickness, label_font_line))
                cnt += 1

            # Get temperature data
            sql = get_deployment_temperature_only_query(devices_list_str, time_from_str, time_to_str, temp_ids_list, temp_offset)
            print(sql)
            with get_db_connection() as conn:
                with conn.cursor() as cur:
                    cur.execute(sql)
                    my_data = cur.fetchall()

                    if my_data != None and len(my_data) > 0:
                        device_to_index = {device: idx for idx, device in enumerate(temp_ids_list)}
                        base_minute = ConvertToBase(time_from_str, time_zone_s)

                        # Process temperature data
                        st = time.time()
                        wave_m = np.zeros((stripes, 1440, 3), dtype=np.uint8)
                        if False:
                            # Simulate data for testing
                            for i in range(min(len(my_data), 500)):
                                if i >= 100:  # Only modify indices 100-500
                                    t = (i - 100) / 4.0  # Temperature value

                                    # Set correct alarm levels based on temperature
                                    if CelsiusToFahrenheit(t) <= 50 or CelsiusToFahrenheit(t) >= 90:
                                        alarm_level = 2  # Critical - should be red
                                    elif CelsiusToFahrenheit(t) <= 60 or CelsiusToFahrenheit(t) >= 80:
                                        alarm_level = 1  # Warning - should be yellow
                                    else:
                                        alarm_level = 0  # Normal - should be green

                                    # Replace the tuple with new values
                                    my_data[i] = (my_data[i][0], my_data[i][1], t, alarm_level)

                        # Create the heatmap data
                        wave_m = create_temperature_optimized_heatmap(my_data, bw, fields_s, wave_m,
                                                             device_to_index, base_minute, time_zone_s,
                                                             min_val, max_val)
                        print(f"Temperature heatmap creation time: {time.time()-st:.4f} seconds")

                        # Stretch the heatmap with different heights for temperature and alarm
                        st = time.time()
                        section_start = vertical_offset + title_labels_height

                        # Loop through each device
                        for device_idx in range(len(temp_ids_list)):
                            # Get the data rows for this device
                            temp_row = wave_m[device_idx * 2]      # Temperature row (even index)
                            alarm_row = wave_m[device_idx * 2 + 1] # Alarm row (odd index)

                            # Calculate the starting y-position for this device
                            device_y_start = section_start + device_idx * temperature_stretch_by

                            # Draw the temperature stripe (15 pixels)
                            for stretch_index in range(temp_stripe_width):
                                target_y = device_y_start + stretch_index
                                if target_y < arr_stretched.shape[0]:
                                    arr_stretched[target_y, labels_width:] = temp_row

                            # Draw the alarm stripe (5 pixels)
                            for stretch_index in range(alarm_stripe_width):
                                target_y = device_y_start + temp_stripe_width + stretch_index
                                if target_y < arr_stretched.shape[0]:
                                    arr_stretched[target_y, labels_width:] = alarm_row

                        print(f"Temperature stretching time: {time.time()-st:.4f} seconds")

        ######################################## HUMIDITY ##################################################################
        '''
        Ideal indoor humidity: 30-50%
        Too dry: Below 30% - Can cause dry skin, irritated eyes, and respiratory issues
        Too humid: Above 60% - Feels warmer than actual temperature, promotes mold growth
        '''
        if show_humidity:
            title_label_text = "HUMIDITY"
            fields_s = ['humidity', 'humidity_state']

            # Define different stripe widths for humidity and alarm

            humidity_offset = 0
            min_val = 30#40
            max_val = 60#60

            # Calculate the correct vertical offset for temperature section
            vertical_offset = 0
            if show_radar:
                vertical_offset += title_labels_height + radar_stripes * radar_stretch_by
            if show_light:
                vertical_offset += title_labels_height + other_sensors_stripes * light_stretch_by
            if show_temperature:
                vertical_offset += title_labels_height + other_sensors_stripes * temperature_stretch_by

            stripes = len(devices_list) * len(fields_s)  # Number of rows needed in data array

            # Draw the temperature section title
            title_text_dimensions = get_text_dimensions(title_label_text, label_font, title_label_font_scale, label_font_thickness)
            title_text_height = title_text_dimensions["height"]
            title_label_width = title_text_dimensions["width"]
            title_label = (title_label_text, (int(labels_width + minutes * 0.5 - title_label_width / 2),
                          vertical_offset + 10 + title_text_height),
                          label_font, title_label_font_scale, title_label_font_color, title_label_font_thickness, label_font_line)

            title_labels.append(title_label)

            # Draw device labels for temperature section
            cnt = 0
            temp_ids_list = []  # Create a separate list for temperature section
            for details in devices_list:
                dev_id = details[0]
                temp_ids_list.append(details[1])
                descriptor = details[2]
                if details[3] != None and details[3] != "":
                    descriptor = descriptor + " " + details[3]
                if details[6] != None and details[6] != "":
                    descriptor = descriptor + " " + details[6]

                text_dimensions = get_text_dimensions(descriptor, label_font, label_font_scale, label_font_thickness)
                text_height = text_dimensions["height"]
                # Position labels in the temperature section
                y_pos = vertical_offset + title_labels_height + text_height + cnt * humidity_stretch_by
                labels.append((descriptor, (10, y_pos), label_font, label_font_scale,
                             label_font_color, label_font_thickness, label_font_line))
                cnt += 1

            # Get humidity data
            sql = get_deployment_humidity_only_query(devices_list_str, time_from_str, time_to_str, temp_ids_list, humidity_offset)
            print(sql)
            with get_db_connection() as conn:
                with conn.cursor() as cur:
                    cur.execute(sql)
                    my_data = cur.fetchall()

                    if my_data != None and len(my_data) > 0:
                        device_to_index = {device: idx for idx, device in enumerate(temp_ids_list)}
                        base_minute = ConvertToBase(time_from_str, time_zone_s)

                        # Process temperature data
                        st = time.time()
                        wave_m = np.zeros((stripes, 1440, 3), dtype=np.uint8)
                        if False:
                            # Simulate data for testing
                            for i in range(min(len(my_data), 500)):
                                if i >= 100:  # Only modify indices 100-500
                                    h = (i - 100) / 4.0  # Temperature value

                                    # Set correct alarm levels based on temperature
                                    if h <= 20 or h >= 60:
                                        alarm_level = 2  # Critical - should be red
                                    elif h <= 30 or h >= 50:
                                        alarm_level = 1  # Warning - should be yellow
                                    else:
                                        alarm_level = 0  # Normal - should be green

                                    # Replace the tuple with new values
                                    my_data[i] = (my_data[i][0], my_data[i][1], h, alarm_level)

                        # Create the heatmap data
                        wave_m = create_humidity_optimized_heatmap(my_data, bw, fields_s, wave_m,
                                                             device_to_index, base_minute, time_zone_s,
                                                             min_val, max_val)
                        print(f"Humidity heatmap creation time: {time.time()-st:.4f} seconds")

                        # Stretch the heatmap with different heights for humidity and alarm
                        st = time.time()
                        section_start = vertical_offset + title_labels_height

                        # Loop through each device
                        for device_idx in range(len(temp_ids_list)):
                            # Get the data rows for this device
                            humidity_row = wave_m[device_idx * 2]      # Humidity row (even index)
                            alarm_row = wave_m[device_idx * 2 + 1] # Alarm row (odd index)

                            # Calculate the starting y-position for this device
                            device_y_start = section_start + device_idx * humidity_stretch_by

                            # Draw the humidity stripe (15 pixels)
                            for stretch_index in range(humidity_stripe_width):
                                target_y = device_y_start + stretch_index
                                if target_y < arr_stretched.shape[0]:
                                    arr_stretched[target_y, labels_width:] = humidity_row

                            # Draw the alarm stripe (5 pixels)
                            for stretch_index in range(alarm_stripe_width):
                                target_y = device_y_start + temp_stripe_width + stretch_index
                                if target_y < arr_stretched.shape[0]:
                                    arr_stretched[target_y, labels_width:] = alarm_row

                        print(f"Temperature stretching time: {time.time()-st:.4f} seconds")

        ######################################## SMELL ##################################################################
        if show_smell:
            title_label_text = "SMELL"
            fields_s = ['S0', 'S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7', 'S8', 'S9']

            # Define different stripe widths for humidity and alarm

            smell_offset = 0

            # Calculate the correct vertical offset for temperature section
            vertical_offset = 0
            if show_radar:
                vertical_offset += title_labels_height + radar_stripes * radar_stretch_by
            if show_light:
                vertical_offset += title_labels_height + other_sensors_stripes * light_stretch_by
            if show_temperature:
                vertical_offset += title_labels_height + other_sensors_stripes * temperature_stretch_by
            if show_humidity:
                vertical_offset += title_labels_height + other_sensors_stripes * humidity_stretch_by

            stripes = len(devices_list) * len(fields_s)  # Number of rows needed in data array

            # Draw the temperature section title
            title_text_dimensions = get_text_dimensions(title_label_text, label_font, title_label_font_scale, label_font_thickness)
            title_text_height = title_text_dimensions["height"]
            title_label_width = title_text_dimensions["width"]
            title_label = (title_label_text, (int(labels_width + minutes * 0.5 - title_label_width / 2),
                          vertical_offset + 10 + title_text_height),
                          label_font, title_label_font_scale, title_label_font_color, title_label_font_thickness, label_font_line)

            title_labels.append(title_label)

            # Draw device labels for temperature section
            cnt = 0
            temp_ids_list = []  # Create a separate list for temperature section
            for details in devices_list:
                dev_id = details[0]
                temp_ids_list.append(details[1])
                descriptor = details[2]
                if details[3] != None and details[3] != "":
                    descriptor = descriptor + " " + details[3]
                if details[6] != None and details[6] != "":
                    descriptor = descriptor + " " + details[6]

                text_dimensions = get_text_dimensions(descriptor, label_font, label_font_scale, label_font_thickness)
                text_height = text_dimensions["height"]
                # Position labels in the temperature section
                y_pos = vertical_offset + title_labels_height +80+ text_height + cnt * smell_component_stretch_by * 10
                #y_pos = vertical_offset + title_labels_height+40+text_height + (cnt)*fields_n*stretch_by)
                labels.append((descriptor, (10, y_pos), label_font, label_font_scale,
                             label_font_color, label_font_thickness, label_font_line))
                cnt += 1

            # Get smell data
            sql = get_deployment_smell_only_query(devices_list_str, time_from_str, time_to_str, temp_ids_list, smell_offset)
            print(sql)
            with get_db_connection() as conn:
                with conn.cursor() as cur:
                    cur.execute(sql)
                    my_data = cur.fetchall()

                    if my_data != None and len(my_data) > 0:
                        device_to_index = {device: idx for idx, device in enumerate(temp_ids_list)}
                        base_minute = ConvertToBase(time_from_str, time_zone_s)

                        # Create the heatmap data
                        create_smell_optimized_heatmap(arr_stretched, my_data, bw, fields_s, device_to_index, base_minute, time_zone_s, smell_component_stretch_by, selected_date, vertical_offset + 18 + title_text_height)



        SaveImageInBlobLabelsOut(image_file, arr_stretched, labels, title_labels)

        print("stop")





def CreateDailyLocationMap(location_image_file, devices_list, selected_date, filter_minutes, time_zone_s, stretch_by):

    devices_c = len(devices_list)
    sensors_c = 1#len(sensors_table)
    time_from_str, time_to_str = GetLocalTimeForDate(selected_date, time_zone_s)

    image_file = location_image_file

    minutes = 1440


    #search_pattern = os.path.join(scriptDir, "scratch/*_"+str(current_date.year)+"_"+str(current_date.month).rjust(2, '0')+"_"+str(current_date.day)+"_*.pkl")

    #allFiles = [os.path.join(dest_path, f) for f in glob.glob(search_pattern)]
    #rekreate .pckl files if missing
    today_date = datetime.datetime.fromtimestamp(time.time())

    arr_source = np.zeros((1, minutes), dtype=float)
    arr_stretched = np.zeros((int(stretch_by), minutes, 3), dtype=np.uint8)  #array to be written as image 3 for RGB channels
    arr_stretched_sorted = np.zeros((int(stretch_by), minutes, 3), dtype=np.uint8)  #array to be written as image 3 for RGB channels

    device_counter = 0
    wave_m = [["", -1] for _ in range(1440)]
    devices_list_str = ','.join(str(device[1]) for device in devices_list)
    ids_list = []
    radar_fields_of_interest = []
    for details in devices_list:
        threshold_str = details[5]
        try:
            threshold_lst = json.loads(threshold_str)
        except:
            threshold_lst = ["s3_max",12]

        radar_field = threshold_lst[0]
        if radar_field not in radar_fields_of_interest:
            radar_fields_of_interest.append(radar_field)

        threshold = threshold_lst[1]
        dev_id =  details[0]
        ids_list.append(details[1])

    sql = get_deployment_radar_only_colapsed_query(devices_list_str, time_from_str, time_to_str, ids_list, radar_fields_of_interest)
    print(sql)
    with get_db_connection() as conn:
        with conn.cursor() as cur:
            cur.execute(sql)
            my_data = cur.fetchall()#cur.fetchone()
            #print(result)
            if my_data == None:
                return False

    device_id_2_threshold = {}
    device_id_2_location = {0: "Outside"}
    row_nr_2_device_id = {}
    cnt = 0
    row_nr_2_device_id[0] = 0
    for details in devices_list:

        well_id, device_id, location_name, description, MAC, radar_threshold_group_st, close_to = details #(266, 559, 'Bathroom', None, '64B70888FAB0', '["s3_max",12]')

        cnt += 1
        row_nr_2_device_id[cnt] = device_id

        if radar_threshold_group_st == None:
            radar_threshold_group_st = '["s3_max",12]' #last value is threshold to s28 composite

        if len(radar_threshold_group_st) > 8:
            radar_threshold_group = json.loads(radar_threshold_group_st)
        else:
            radar_threshold_group = ["s3_max",12]

        device_id_2_location[device_id] = location_name

        device_id_2_threshold[device_id] = radar_threshold_group

    target_tz = pytz.timezone(time_zone_s)
    st = time.time()

    #each record in my_data has time, device_id and radar_fields_of_interest in it

    try:
        result_np = process_wave_data_numpy(image_file, my_data, time_zone_s, device_id_2_threshold, radar_fields_of_interest)
        print(time.time() - st)
    except Exception as err:
        print(str(err))

    if False:
        for record in my_data:
            time_val, device_id, min_val, max_val = record
            radar_threshold = device_id_2_threshold[device_id]
            local_time = time_val.astimezone(target_tz)
            minute_m = int((local_time - local_time.replace(hour=0, minute=0, second=0, microsecond=0)).total_seconds() / 60)

            if (wave_m[minute_m][0] == ""):
                if max_val > radar_threshold:
                    wave_m[minute_m][0] = device_id
                    wave_m[minute_m][1] = max_val
            else:
                if max_val > radar_threshold:
                    if max_val > wave_m[minute_m][1]:
                        wave_m[minute_m][0] = device_id
                        wave_m[minute_m][1] = max_val



    print(time.time()-st)
    wave_m = result_np
    rgb_row = np.zeros(( 1440, 3), dtype=np.uint8)
    rgbsorted_row = np.zeros(( 1440, 3), dtype=np.uint8)
    #wave_m = FilterGlitches(wave_m, filter_minutes)
    r = 0
    g = 0
    b = 0

    presence_minutes = {}

    #we want to generate present_at array
    if isinstance(wave_m[0], np.int64):
        inital_device_id = row_nr_2_device_id[wave_m[0]]
    else:
        inital_device_id = 0
    present_at = [[inital_device_id, 0, 1]] #device_id, minute, duration
    for minute_m in range(1440):
        try:
            if isinstance(wave_m[minute_m], np.int64):
                device_id = row_nr_2_device_id[wave_m[minute_m]]
            else:
                device_id = 0

            if device_id != "" and device_id != -1:
                r,g,b = Loc2Color[device_id_2_location[device_id]][0]
                rgb_row[minute_m] = b,g,r

            if Loc2Color[device_id_2_location[device_id]][1] in presence_minutes:
                presence_minutes[Loc2Color[device_id_2_location[device_id]][1]] =  [presence_minutes[Loc2Color[device_id_2_location[device_id]][1]][0] + 1, Loc2Color[device_id_2_location[device_id]][0]]
            else:
                presence_minutes[Loc2Color[device_id_2_location[device_id]][1]] = [1, Loc2Color[device_id_2_location[device_id]][0]]

            if minute_m > 0:
                if present_at[-1][0] != device_id:
                    present_at.append([device_id, minute_m, 1])
                else:
                    present_at[-1][2] += 1

        except Exception as err:
            print(str(err))

    start_minute = 0
    for color_key in sorted(presence_minutes):
        print(color_key, presence_minutes[color_key])
        rgbsorted_row[start_minute:start_minute+presence_minutes[color_key][0]] = presence_minutes[color_key][1][::-1]
        start_minute += presence_minutes[color_key][0]

    #we need to save present_at list to blob
    SaveObjectInBlob(image_file+".bin", present_at)
    #present_at_back_s = ReadObjectMinIO("daily-maps", image_file+".bin")
    #present_at_back = json.loads(present_at_back_s)
    #print(present_at_back)
    for stretch_index in range(stretch_by):
        y = stretch_index
        arr_stretched[y, :] = rgb_row
        arr_stretched_sorted[y, :] = rgbsorted_row
    #print("stop")
    #print(r,g,b)
    SaveImageInBlob(image_file, arr_stretched)
    SaveImageInBlob(image_file[:-4]+"S.png", arr_stretched_sorted)


def GenerateFullLocationMap(map_file, deployment_id, ddate, recreate_or_not, chart_type, bw, motion, scale_global, fast, time_zone_s, filter_minutes = 5):

    timee = LocalDateToUTCEpoch(ddate, time_zone_s)+5 #add so date boundary is avoided
    devices_list, device_ids =  GetProximityList(deployment_id, timee)

    st = time.time()
    if CreateFullLocationMap(map_file, devices_list, ddate, 1, recreate_or_not, chart_type, bw, motion, scale_global, fast, filter_minutes, time_zone_s) == 0: #"[bit] 1=same sensors together, 2=same device together, 4=1 der, 8=2 der
        print(ddate, "Not found")
    else:
        print(ddate, time.time() - st)

def GenerateFullLocationMapLabelsOut(map_file, deployment_id, ddate, recreate_or_not, chart_type, bw, motion, scale_global, fast, time_zone_s, filter_minutes = 5):

    timee = LocalDateToUTCEpoch(ddate, time_zone_s)+5 #add so date boundary is avoided
    devices_list, device_ids =  GetProximityList(deployment_id, timee)

    st = time.time()
    if CreateFullLocationMapLabelsOut(map_file, devices_list, ddate, 1, recreate_or_not, chart_type, bw, motion, scale_global, fast, filter_minutes, time_zone_s) == 0: #"[bit] 1=same sensors together, 2=same device together, 4=1 der, 8=2 der
        print(ddate, "Not found")
    else:
        print(ddate, time.time() - st)

def CreateMapFast(map_file, devices_list, selected_date, bw, time_zone_s, radar_part, group_by):
    global Id2MACDict

    st = time.time()
    if radar_part == "s28":
        radar_part = "(s2+s3+s4+s5+s6+s7+s8)/7"
    try:

        #stretch_to_min_max = True

        #current_date_p = selected_date.replace("-", "_")
        #current_date_s = selected_date

        lower_than200 = 0
        larger_than200 = 0
        ids_list = []
        for details in devices_list[0]:
            dev_id =  details[0]
            ids_list.append(details[1])
            if dev_id < 200:
                lower_than200 += 1
            else:
                larger_than200 += 1

        if lower_than200 > 0 and larger_than200 > 0:
            return False, []

        if larger_than200 > 0:
            sensors_c = len(s_table)
        else: #old sensors not supported
            return False, []


        devices_c = len(devices_list[0])
        devices_list_str = ",".join(map(str, devices_list[1]))
        image_file = map_file
        time_from_str, time_to_str = GetLocalTimeForDate(selected_date, time_zone_s)
        temp_offset = -16
        sql = get_deployment_query(devices_list_str, time_from_str, time_to_str, ids_list, radar_part, temp_offset)
        print(sql)
        #print(sql)
        #st = time.time()
        print(f"@1 ----{time.time() - st}")
        with get_db_connection() as conn:
            print(f"@1a ----{time.time() - st}")
            with conn.cursor() as cur:
                print(f"@1b ----{time.time() - st}")
                cur.execute(sql)
                day_data = cur.fetchall()#cur.fetchone()
                #print(result)
                if day_data == None:
                    print(f"@1c ----{time.time() - st}")
                    return False, []
        print(f"@2 ----{time.time() - st}")

        stretch_by = 10
        minutes = 1440
        stripes = devices_c * sensors_c #2 for upper maxes, lower mins
        arr_source_template = np.full((stripes, minutes+4), -0.001, dtype=float)
        print(f"@3 ----{time.time() - st}")
        st = time.time()
        arr_stretched_template = np.zeros((int(stripes*stretch_by), minutes, 3), dtype=np.uint8)  # 3 for RGB channels
        print(f"@4a ----{time.time() - st}")
        #st = time.time()
        #arr_source = fill_array_from_timescale(day_data, time_from_str, devices_list[1], arr_source_template, time_zone_s)
        #print(f"@4b ----{time.time() - st}")
        #st = time.time()
        #arr_source = fast_fill_array_from_timescale_bad(day_data, time_from_str, devices_list[1], arr_source_template, time_zone_s)
        #print(f"@4n ----{time.time() - st}")
        st = time.time()
        arr_source = fast_fill_array_from_timescale(day_data, time_from_str, devices_list[1], arr_source_template, time_zone_s)
        #arr_source = fill_array_from_timescale(day_data, time_from_str, devices_list[1], arr_source_template, time_zone_s)
        print(f"@5 ----{time.time() - st}")
        arr_source = AddLimits_optimized(arr_source, devices_c, sensors_c, percentile=100)
        print(f"@6 ----{time.time() - st}")
        scaled_day = CalcExtremes(arr_source, minutes, stripes)
        print(f"@7 ----{time.time() - st}")
        arr_stretched, vocs_scaled = FillImage_optimized(scaled_day, devices_c, sensors_c, arr_stretched_template, group_by, bw)
        print(f"@8 ----{time.time() - st}")
        SaveImageInBlob(image_file, arr_stretched)
        print(f"@9 ----{time.time() - st}")
        return True, vocs_scaled

    except Exception as e:
        AddToLog(traceback.format_exc())
        return False, []

def get_deployment_query(devices_list_str, time_from_str, time_to_str, ids_list, radar_part, temp_offset):
    """
    Generate a TimeScaleDB query for sensor and radar readings based on device IDs.

    Parameters:
    devices_list_str (str): Comma-separated string of device IDs
    time_from_str (str): Start time for the query
    time_to_str (str): End time for the query
    ids_list (list): List of device IDs in priority order for sorting
    radar_part (str): Radar column name, defaults to 'radar'

    Returns:
    str: Generated SQL query
    """

    # Generate the CASE statement for ordering based on the provided ids_list
    case_statements = []
    for index, device_id in enumerate(ids_list, start=1):
        case_statements.append(f"WHEN {device_id} THEN {index}")

    case_order = "\n                ".join(case_statements)

    sql = f"""
        SELECT
            COALESCE(sr.minute, rr.minute) as minute,
            COALESCE(sr.device_id, rr.device_id) as device_id,
            sr.avg_temperature+ {temp_offset} as avg_temperature,
            sr.avg_humidity,
            sr.pressure_amplitude,
            sr.max_light,
            rr.radar,
            sr.min_s0 as sensor_min_s0,
            sr.min_s1 as sensor_min_s1,
            sr.min_s2 as sensor_min_s2,
            sr.min_s3 as sensor_min_s3,
            sr.min_s4 as sensor_min_s4,
            sr.min_s5 as sensor_min_s5,
            sr.min_s6 as sensor_min_s6,
            sr.min_s7 as sensor_min_s7,
            sr.min_s8 as sensor_min_s8,
            sr.min_s9 as sensor_min_s9
        FROM (
            SELECT
                time_bucket('1 minute', time) AS minute,
                device_id,
                AVG(temperature) AS avg_temperature,
                AVG(humidity) AS avg_humidity,
                AVG(pressure) AS pressure_amplitude,
                MAX(light) AS max_light,
                MIN(CASE WHEN s0 > 0 THEN s0 END) AS min_s0,
                MIN(CASE WHEN s1 > 0 THEN s1 END) AS min_s1,
                MIN(CASE WHEN s2 > 0 THEN s2 END) AS min_s2,
                MIN(CASE WHEN s3 > 0 THEN s3 END) AS min_s3,
                MIN(CASE WHEN s4 > 0 THEN s4 END) AS min_s4,
                MIN(CASE WHEN s5 > 0 THEN s5 END) AS min_s5,
                MIN(CASE WHEN s6 > 0 THEN s6 END) AS min_s6,
                MIN(CASE WHEN s7 > 0 THEN s7 END) AS min_s7,
                MIN(CASE WHEN s8 > 0 THEN s8 END) AS min_s8,
                MIN(CASE WHEN s9 > 0 THEN s9 END) AS min_s9
            FROM
                sensor_readings
            WHERE
                device_id IN ({devices_list_str})
                AND time >= '{time_from_str}'
                AND time < '{time_to_str}'
            GROUP BY
                minute,
                device_id
        ) sr
        FULL OUTER JOIN (
            SELECT
                time_bucket('1 minute', time) AS minute,
                device_id,
                MAX({radar_part}) AS radar
            FROM
                radar_readings
            WHERE
                device_id IN ({devices_list_str})
                AND time >= '{time_from_str}'
                AND time < '{time_to_str}'
            GROUP BY
                minute,
                device_id
        ) rr
        ON sr.minute = rr.minute AND sr.device_id = rr.device_id
        ORDER BY
            CASE COALESCE(sr.device_id, rr.device_id)
                {case_order}
            END,
            COALESCE(sr.minute, rr.minute);
        """
    return sql

def get_deployment_rd_query(devices_list_str, time_from_str, time_to_str, ids_list, temp_offset):
    #radar detailed
    """
    Generate a TimeScaleDB query for sensor and radar readings based on device IDs.

    Parameters:
    devices_list_str (str): Comma-separated string of device IDs
    time_from_str (str): Start time for the query
    time_to_str (str): End time for the query
    ids_list (list): List of device IDs in priority order for sorting

    Returns:
    str: Generated SQL query
    """

    # Generate the CASE statement for ordering based on the provided ids_list
    case_statements = []
    for index, device_id in enumerate(ids_list, start=1):
        case_statements.append(f"WHEN {device_id} THEN {index}")

    case_order = "\n                ".join(case_statements)

    sql = f"""
        SELECT
            COALESCE(sr.minute, rr.minute) as minute,
            COALESCE(sr.device_id, rr.device_id) as device_id,
            sr.avg_temperature+{temp_offset} as avg_temperature,
            sr.avg_humidity,
            sr.avg_pressure,
            sr.max_light,
            sr.min_s0 as smell_s0,
            sr.min_s1 as smell_s1,
            sr.min_s2 as smell_s2,
            sr.min_s3 as smell_s3,
            sr.min_s4 as smell_s4,
            sr.min_s5 as smell_s5,
            sr.min_s6 as smell_s6,
            sr.min_s7 as smell_s7,
            sr.min_s8 as smell_s8,
            sr.min_s9 as smell_s9,
            rr.absent as radar_absent,
            rr.moving as radar_moving,
            rr.stationary as radar_stationary,
            rr.both as radar_both,
            rr.m0 as radar_m0,
            rr.m1 as radar_m1,
            rr.m2 as radar_m2,
            rr.m3 as radar_m3,
            rr.m4 as radar_m4,
            rr.m5 as radar_m5,
            rr.m6 as radar_m6,
            rr.m7 as radar_m7,
            rr.m8 as radar_m8,
            rr.s2 as radar_s2,
            rr.s3 as radar_s3,
            rr.s4 as radar_s4,
            rr.s5 as radar_s5,
            rr.s6 as radar_s6,
            rr.s7 as radar_s7,
            rr.s8 as radar_s8
        FROM (
            SELECT
                time_bucket('1 minute', time) AS minute,
                device_id,
                AVG(temperature) AS avg_temperature,
                AVG(humidity) AS avg_humidity,
                AVG(pressure) AS avg_pressure,
                MAX(light) AS max_light,
                MIN(CASE WHEN s0 > 0 THEN s0 END) AS min_s0,
                MIN(CASE WHEN s1 > 0 THEN s1 END) AS min_s1,
                MIN(CASE WHEN s2 > 0 THEN s2 END) AS min_s2,
                MIN(CASE WHEN s3 > 0 THEN s3 END) AS min_s3,
                MIN(CASE WHEN s4 > 0 THEN s4 END) AS min_s4,
                MIN(CASE WHEN s5 > 0 THEN s5 END) AS min_s5,
                MIN(CASE WHEN s6 > 0 THEN s6 END) AS min_s6,
                MIN(CASE WHEN s7 > 0 THEN s7 END) AS min_s7,
                MIN(CASE WHEN s8 > 0 THEN s8 END) AS min_s8,
                MIN(CASE WHEN s9 > 0 THEN s9 END) AS min_s9
            FROM
                sensor_readings
            WHERE
                device_id IN ({devices_list_str})
                AND time >= '{time_from_str}'
                AND time < '{time_to_str}'
            GROUP BY
                minute,
                device_id
        ) sr
        FULL OUTER JOIN (
            SELECT
                time_bucket('1 minute', time) AS minute,
                device_id,
                MAX(absent) AS absent,
                MAX(moving) AS moving,
                MAX(stationary) AS stationary,
                MAX(\"both\") AS both,
                MAX(m0) AS m0,
                MAX(m1) AS m1,
                MAX(m2) AS m2,
                MAX(m3) AS m3,
                MAX(m4) AS m4,
                MAX(m5) AS m5,
                MAX(m6) AS m6,
                MAX(m7) AS m7,
                MAX(m8) AS m8,
                MAX(s2) AS s2,
                MAX(s3) AS s3,
                MAX(s4) AS s4,
                MAX(s5) AS s5,
                MAX(s6) AS s6,
                MAX(s7) AS s7,
                MAX(s8) AS s8
            FROM
                radar_readings
            WHERE
                device_id IN ({devices_list_str})
                AND time >= '{time_from_str}'
                AND time < '{time_to_str}'
            GROUP BY
                minute,
                device_id
        ) rr
        ON sr.minute = rr.minute AND sr.device_id = rr.device_id
        ORDER BY
            CASE COALESCE(sr.device_id, rr.device_id)
                {case_order}
            END,
            COALESCE(sr.minute, rr.minute);
        """
    return sql

def get_deployment_radar_only_query(devices_list_str, time_from_str, time_to_str, ids_list):
    #radar detailed
    """
    Generate a TimeScaleDB query for sensor and radar readings based on device IDs.

    Parameters:
    devices_list_str (str): Comma-separated string of device IDs
    time_from_str (str): Start time for the query
    time_to_str (str): End time for the query
    ids_list (list): List of device IDs in priority order for sorting

    Returns:
    str: Generated SQL query
    """

    # Generate the CASE statement for ordering based on the provided ids_list
    case_statements = []
    for index, device_id in enumerate(ids_list, start=1):
        case_statements.append(f"WHEN {device_id} THEN {index}")

    case_order = "\n                ".join(case_statements)

    sql = f"""
        SELECT
            COALESCE(sr.minute, rr.minute) as minute,
            COALESCE(sr.device_id, rr.device_id) as device_id,
            rr.s2 as radar_s2,
            rr.s3 as radar_s3,
            rr.s4 as radar_s4,
            rr.s5 as radar_s5,
            rr.s6 as radar_s6,
            rr.s7 as radar_s7,
            rr.s8 as radar_s8
        FROM (
            SELECT
                time_bucket('1 minute', time) AS minute,
                device_id
            FROM
                sensor_readings
            WHERE
                device_id IN ({devices_list_str})
                AND time >= '{time_from_str}'
                AND time < '{time_to_str}'
            GROUP BY
                minute,
                device_id
        ) sr
        FULL OUTER JOIN (
            SELECT
                time_bucket('1 minute', time) AS minute,
                device_id,
                MAX(s2) AS s2,
                MAX(s3) AS s3,
                MAX(s4) AS s4,
                MAX(s5) AS s5,
                MAX(s6) AS s6,
                MAX(s7) AS s7,
                MAX(s8) AS s8
            FROM
                radar_readings
            WHERE
                device_id IN ({devices_list_str})
                AND time >= '{time_from_str}'
                AND time < '{time_to_str}'
            GROUP BY
                minute,
                device_id
        ) rr
        ON sr.minute = rr.minute AND sr.device_id = rr.device_id
        ORDER BY
            CASE COALESCE(sr.device_id, rr.device_id)
                {case_order}
            END,
            COALESCE(sr.minute, rr.minute);
        """
    return sql

def get_device_radar_s28_only_query(time_from_str, time_to_str, device_id):
    sql = f"""
    SELECT
        time,
        (s2+s3+s4+s5+s6+s7+s8)/7 AS s28,
        (m2+m3+m4+m5+m6+m7+m8)/7 AS m28
    FROM
        radar_readings
    WHERE
        device_id = {device_id}
        AND time >= '{time_from_str}'
        AND time < '{time_to_str}'
    ORDER BY
            time ASC
        """
    return sql

def get_device_radar_only_query(devices_list_str, time_from_str, time_to_str, device_id):
    sql = f"""
    SELECT
        time,
        (s2+s3+s4+s5+s6+s7+s8)/7 AS s28,
        (m2+m3+m4+m5+m6+m7+m8)/7 AS m28
    FROM
        radar_readings
    WHERE
        device_id IN ({devices_list_str})
        AND time >= '{time_from_str}'
        AND time < '{time_to_str}'
    ORDER BY
            time ASC
        """
    return sql


def get_deployment_radar_only_colapsed_query(devices_list_str, time_from_str, time_to_str, ids_list, radar_fields_of_interest):
    #radar detailed
    """
    Generate a TimeScaleDB query for sensor and radar readings based on device IDs.

    Parameters:
    devices_list_str (str): Comma-separated string of device IDs
    time_from_str (str): Start time for the query
    time_to_str (str): End time for the query
    ids_list (list): List of device IDs in priority order for sorting

    Returns:
    str: Generated SQL query
    """

    # Generate the CASE statement for ordering based on the provided ids_list
    case_statements = []
    for index, device_id in enumerate(ids_list, start=1):
        case_statements.append(f"WHEN {device_id} THEN {index}")

    case_order = "\n                ".join(case_statements)
    radar_fields_to_get = ""
    q_parts = ""
    for field in radar_fields_of_interest:
        if field == "s28_min":
            q_part = "MIN((s2+s3+s4+s5+s6+s7+s8)/7) AS s28_min"
        elif field == "s28_max":
            q_part = "MAX((s2+s3+s4+s5+s6+s7+s8)/7) AS s28_max"
        elif field == "m08_max":
            q_part = "MAX((m0+m1+m2+m3+m4+m5+m6+m7+m8)/9) AS m08_max"
        elif field == "s2_max":
            q_part = "MAX(s2) AS s2_max"
        elif field == "s3_max":
            q_part = "MAX(s3) AS s3_max"
        elif field == "s4_max":
            q_part = "MAX(s4) AS s4_max"
        elif field == "s5_max":
            q_part = "MAX(s5) AS s5_max"
        elif field == "s6_max":
            q_part = "MAX(s6) AS s6_max"
        elif field == "s7_max":
            q_part = "MAX(s7) AS s7_max"
        elif field == "s8_max":
            q_part = "MAX(s8) AS s8_max"
        elif field == "m0_max":
            q_part = "MAX(m0) AS m0_max"
        elif field == "m1_max":
            q_part = "MAX(m1) AS m1_max"
        elif field == "m2_max":
            q_part = "MAX(m2) AS m2_max"
        elif field == "m3_max":
            q_part = "MAX(m3) AS m3_max"
        elif field == "m4_max":
            q_part = "MAX(m4) AS m4_max"
        elif field == "m5_max":
            q_part = "MAX(m5) AS m5_max"
        elif field == "m6_max":
            q_part = "MAX(m6) AS m6_max"
        elif field == "m7_max":
            q_part = "MAX(m7) AS m7_max"
        elif field == "m8_max":
            q_part = "MAX(m8) AS m8_max"
        else:
            q_part = field

        if q_parts == "":
            q_parts = q_part
        else:
            q_parts = q_parts + ", " + q_part
        if radar_fields_to_get == "":
            radar_fields_to_get = field
        else:
            radar_fields_to_get = radar_fields_to_get + ", " + field
    sql = f"""
    SELECT
        minute,
        device_id,
        {radar_fields_to_get}
    FROM (
        SELECT
            time_bucket('1 minute', time) AS minute,
            device_id,
            {q_parts}
        FROM
            radar_readings
        WHERE
                device_id IN ({devices_list_str})
                AND time >= '{time_from_str}'
                AND time < '{time_to_str}'
            GROUP BY
                minute,
                device_id
        ) rr

        ORDER BY
            CASE device_id
                {case_order}
            END,
            minute
        """
    return sql

def get_deployment_radar_only_detailed_query(devices_list_str, time_from_str, time_to_str, ids_list):
    #radar detailed
    """
    Generate a TimeScaleDB query for sensor and radar readings based on device IDs.

    Parameters:
    devices_list_str (str): Comma-separated string of device IDs
    time_from_str (str): Start time for the query
    time_to_str (str): End time for the query
    ids_list (list): List of device IDs in priority order for sorting

    Returns:
    str: Generated SQL query
    This is looking for presence, NOT absence... othervise all MAXes need to be converted to MINs
    """

    # Generate the CASE statement for ordering based on the provided ids_list
    case_statements = []
    for index, device_id in enumerate(ids_list, start=1):
        case_statements.append(f"WHEN {device_id} THEN {index}")

    case_order = "\n                ".join(case_statements)

    sql = f"""
    SELECT
        minute,
        device_id,
        m0_max,
        m1_max,
        m2_max,
        m3_max,
        m4_max,
        m5_max,
        m6_max,
        m7_max,
        m8_max,
        m08_max,
        s2_max,
        s3_max,
        s4_max,
        s5_max,
        s6_max,
        s7_max,
        s8_max,
        s28_max,
        s28_min
    FROM (
        SELECT
            time_bucket('1 minute', time) AS minute,
            device_id,
            MAX(m0) AS m0_max,
            MAX(m1) AS m1_max,
            MAX(m2) AS m2_max,
            MAX(m3) AS m3_max,
            MAX(m4) AS m4_max,
            MAX(m5) AS m5_max,
            MAX(m6) AS m6_max,
            MAX(m7) AS m7_max,
            MAX(m8) AS m8_max,
            MAX((m0+m1+m2+m3+m4+m5+m6+m7+m8)/9) AS m08_max,
            MAX(s2) AS s2_max,
            MAX(s3) AS s3_max,
            MAX(s4) AS s4_max,
            MAX(s5) AS s5_max,
            MAX(s6) AS s6_max,
            MAX(s7) AS s7_max,
            MAX(s8) AS s8_max,
            MAX((s2+s3+s4+s5+s6+s7+s8)/7) AS s28_max,
            MIN((s2+s3+s4+s5+s6+s7+s8)/7) AS s28_min
        FROM
            radar_readings
        WHERE
                device_id IN ({devices_list_str})
                AND time >= '{time_from_str}'
                AND time < '{time_to_str}'
            GROUP BY
                minute,
                device_id
        ) rr

        ORDER BY
            CASE device_id
                {case_order}
            END,
            minute
        """
    return sql

def get_deployment_light_only_query(devices_list_str, time_from_str, time_to_str, ids_list):
    #light detailed
    """
    Generate a TimeScaleDB query for light readings based on device IDs.

    Parameters:
    devices_list_str (str): Comma-separated string of device IDs
    time_from_str (str): Start time for the query
    time_to_str (str): End time for the query
    ids_list (list): List of device IDs in priority order for sorting

    Returns:
    str: Generated SQL query
    """

    # Generate the CASE statement for ordering based on the provided ids_list
    case_statements = []
    for index, device_id in enumerate(ids_list, start=1):
        case_statements.append(f"WHEN {device_id} THEN {index}")

    case_order = "\n                ".join(case_statements)

    sql = f"""
    SELECT
        minute,
        device_id,
        light_max
    FROM (
        SELECT
            time_bucket('1 minute', time) AS minute,
            device_id,
            MAX(light) AS light_max
        FROM
            sensor_readings
        WHERE
                device_id IN ({devices_list_str})
                AND time >= '{time_from_str}'
                AND time < '{time_to_str}'
            GROUP BY
                minute,
                device_id
        ) rr

        ORDER BY
            CASE device_id
                {case_order}
            END,
            minute
        """
    return sql

def get_deployment_temperature_only_query(devices_list_str, time_from_str, time_to_str, ids_list, temp_offset):
    """
    Generate a TimeScaleDB query for temperature readings based on device IDs.

    Parameters:
    devices_list_str (str): Comma-separated string of device IDs
    time_from_str (str): Start time for the query
    time_to_str (str): End time for the query
    ids_list (list): List of device IDs in priority order for sorting
    temp_offset (float): Temperature offset to apply

    Returns:
    str: Generated SQL query with temperature and alarm level
    """
    # Generate the CASE statement for ordering based on the provided ids_list
    case_statements = []
    for index, device_id in enumerate(ids_list, start=1):
        case_statements.append(f"WHEN {device_id} THEN {index}")
    case_order = "\n                ".join(case_statements)

    # SQL query with added alarm_level calculation
    sql = f"""
    SELECT
        minute,
        device_id,
        temperature_avg,
        CASE
            WHEN (temperature_avg * 9/5 + 32) <= 50 OR (temperature_avg * 9/5 + 32) >= 90 THEN 2
            WHEN (temperature_avg * 9/5 + 32) <= 60 OR (temperature_avg * 9/5 + 32) >= 80 THEN 1
            ELSE 0
        END AS alarm_level
    FROM (
        SELECT
            time_bucket('1 minute', time) AS minute,
            device_id,
            AVG(temperature)+{temp_offset} AS temperature_avg
        FROM
            sensor_readings
        WHERE
                device_id IN ({devices_list_str})
                AND time >= '{time_from_str}'
                AND time < '{time_to_str}'
            GROUP BY
                minute,
                device_id
        ) rr

        ORDER BY
            CASE device_id
                {case_order}
            END,
            minute
        """
    return sql

def get_deployment_humidity_only_query(devices_list_str, time_from_str, time_to_str, ids_list, humidity_offset):
    """
    Generate a TimeScaleDB query for humidity readings based on device IDs.

    Parameters:
    devices_list_str (str): Comma-separated string of device IDs
    time_from_str (str): Start time for the query
    time_to_str (str): End time for the query
    ids_list (list): List of device IDs in priority order for sorting
    temp_offset (float): Temperature offset to apply

    Returns:
    str: Generated SQL query with humidity and alarm level
    """
    # Generate the CASE statement for ordering based on the provided ids_list
    case_statements = []
    for index, device_id in enumerate(ids_list, start=1):
        case_statements.append(f"WHEN {device_id} THEN {index}")
    case_order = "\n                ".join(case_statements)
    col_expr = f"2.3592 * AVG(humidity) + 23.5546" #= 2.3592 * J2 + 33.5546

    # SQL query with added alarm_level calculation
    sql = f"""
    SELECT
        minute,
        device_id,
        humidity_avg,
        CASE
            WHEN humidity_avg <= 20 OR humidity_avg >= 60 THEN 2
            WHEN humidity_avg <= 30 OR humidity_avg >= 50 THEN 1
            ELSE 0
        END AS alarm_level
    FROM (
        SELECT
            time_bucket('1 minute', time) AS minute,
            device_id,
            {col_expr} AS humidity_avg
        FROM
            sensor_readings
        WHERE
                device_id IN ({devices_list_str})
                AND time >= '{time_from_str}'
                AND time < '{time_to_str}'
            GROUP BY
                minute,
                device_id
        ) rr

        ORDER BY
            CASE device_id
                {case_order}
            END,
            minute
        """
    return sql

def get_deployment_smell_only_query(devices_list_str, time_from_str, time_to_str, ids_list, humidity_offset):
    """
    Generate a TimeScaleDB query for smell readings based on device IDs.

    Parameters:
    devices_list_str (str): Comma-separated string of device IDs
    time_from_str (str): Start time for the query
    time_to_str (str): End time for the query
    ids_list (list): List of device IDs in priority order for sorting
    temp_offset (float): Temperature offset to apply

    Returns:
    str: Generated SQL query with smell components
    """
    # Generate the CASE statement for ordering based on the provided ids_list
    case_statements = []
    for index, device_id in enumerate(ids_list, start=1):
        case_statements.append(f"WHEN {device_id} THEN {index}")
    case_order = "\n                ".join(case_statements)

    # SQL query with added alarm_level calculation
    sql = f"""
    SELECT
        minute,
        device_id,
        min_s0,
        min_s1,
        min_s2,
        min_s3,
        min_s4,
        min_s5,
        min_s6,
        min_s7,
        min_s8,
        min_s9
    FROM (
        SELECT
            time_bucket('1 minute', time) AS minute,
            device_id,
            MIN(CASE WHEN s0 > 0 THEN s0 END) AS min_s0,
            MIN(CASE WHEN s1 > 0 THEN s1 END) AS min_s1,
            MIN(CASE WHEN s2 > 0 THEN s2 END) AS min_s2,
            MIN(CASE WHEN s3 > 0 THEN s3 END) AS min_s3,
            MIN(CASE WHEN s4 > 0 THEN s4 END) AS min_s4,
            MIN(CASE WHEN s5 > 0 THEN s5 END) AS min_s5,
            MIN(CASE WHEN s6 > 0 THEN s6 END) AS min_s6,
            MIN(CASE WHEN s7 > 0 THEN s7 END) AS min_s7,
            MIN(CASE WHEN s8 > 0 THEN s8 END) AS min_s8,
            MIN(CASE WHEN s9 > 0 THEN s9 END) AS min_s9
        FROM
            sensor_readings
        WHERE
                device_id IN ({devices_list_str})
                AND time >= '{time_from_str}'
                AND time < '{time_to_str}'
            GROUP BY
                minute,
                device_id
        ) rr

        ORDER BY
            CASE device_id
                {case_order}
            END,
            minute
        """
    return sql


def get_deployment_radar_only_detailed_all_query(devices_list_str, time_from_str, time_to_str, ids_list):
    #radar detailed
    """
    Generate a TimeScaleDB query for sensor and radar readings based on device IDs.

    Parameters:
    devices_list_str (str): Comma-separated string of device IDs
    time_from_str (str): Start time for the query
    time_to_str (str): End time for the query
    ids_list (list): List of device IDs in priority order for sorting

    Returns:
    str: Generated SQL query
    This is looking for presence, NOT absence... othervise all MAXes need to be converted to MINs
    """

    # Generate the CASE statement for ordering based on the provided ids_list
    case_statements = []
    for index, device_id in enumerate(ids_list, start=1):
        case_statements.append(f"WHEN {device_id} THEN {index}")

    case_order = "\n                ".join(case_statements)

    sql = f"""
    SELECT
        minute,
        device_id,
        absent_min,
        stationary_max,
        moving_max,
        both_max,
        m0_max,
        m1_max,
        m2_max,
        m3_max,
        m4_max,
        m5_max,
        m6_max,
        m7_max,
        m8_max,
        m08_max,
        s2_max,
        s3_max,
        s4_max,
        s5_max,
        s6_max,
        s7_max,
        s8_max,
        s28_max
    FROM (
        SELECT
            time_bucket('1 minute', time) AS minute,
            device_id,
            MIN(absent) AS absent_min,
            MAX(stationary) AS stationary_max,
            MAX(moving) AS moving_max,
            MAX("both") AS both_max,
            MAX(m0) AS m0_max,
            MAX(m1) AS m1_max,
            MAX(m2) AS m2_max,
            MAX(m3) AS m3_max,
            MAX(m4) AS m4_max,
            MAX(m5) AS m5_max,
            MAX(m6) AS m6_max,
            MAX(m7) AS m7_max,
            MAX(m8) AS m8_max,
            MAX((m0+m1+m2+m3+m4+m5+m6+m7+m8)/9) AS m08_max,
            MAX(s2) AS s2_max,
            MAX(s3) AS s3_max,
            MAX(s4) AS s4_max,
            MAX(s5) AS s5_max,
            MAX(s6) AS s6_max,
            MAX(s7) AS s7_max,
            MAX(s8) AS s8_max,
            MAX((s2+s3+s4+s5+s6+s7+s8)/7) AS s28_max
        FROM
            radar_readings
        WHERE
                device_id IN ({devices_list_str})
                AND time >= '{time_from_str}'
                AND time < '{time_to_str}'
            GROUP BY
                minute,
                device_id
        ) rr

        ORDER BY
            CASE device_id
                {case_order}
            END,
            minute
        """
    return sql
def get_deployment_deca_query(devices_list_str, time_from_str, time_to_str, ids_list, temp_offset):
    """
    Generate a TimeScaleDB query for sensor and radar readings based on device IDs.

    Parameters:
    devices_list_str (str): Comma-separated string of device IDs
    time_from_str (str): Start time for the query
    time_to_str (str): End time for the query
    ids_list (list): List of device IDs in priority order for sorting

    Returns:
    str: Generated SQL query
    """

    # Generate the CASE statement for ordering based on the provided ids_list
    case_statements = []
    for index, device_id in enumerate(ids_list, start=1):
        case_statements.append(f"WHEN {device_id} THEN {index}")

    case_order = "\n                ".join(case_statements)

    sql = f"""
        SELECT
            COALESCE(sr.minute, rr.minute) as minute,
            COALESCE(sr.device_id, rr.device_id) as device_id,
            sr.avg_temperature+{temp_offset} as avg_temperature,
            sr.avg_humidity,
            sr.avg_pressure,
            sr.max_light,
            sr.min_s0 as smell_s0,
            sr.min_s1 as smell_s1,
            sr.min_s2 as smell_s2,
            sr.min_s3 as smell_s3,
            sr.min_s4 as smell_s4,
            sr.min_s5 as smell_s5,
            sr.min_s6 as smell_s6,
            sr.min_s7 as smell_s7,
            sr.min_s8 as smell_s8,
            sr.min_s9 as smell_s9,
            rr.absent as radar_absent,
            rr.moving as radar_moving,
            rr.stationary as radar_stationary,
            rr.both as radar_both,
            rr.m0 as radar_m0,
            rr.m1 as radar_m1,
            rr.m2 as radar_m2,
            rr.m3 as radar_m3,
            rr.m4 as radar_m4,
            rr.m5 as radar_m5,
            rr.m6 as radar_m6,
            rr.m7 as radar_m7,
            rr.m8 as radar_m8,
            rr.s2 as radar_s2,
            rr.s3 as radar_s3,
            rr.s4 as radar_s4,
            rr.s5 as radar_s5,
            rr.s6 as radar_s6,
            rr.s7 as radar_s7,
            rr.s8 as radar_s8
        FROM (
            SELECT
                time_bucket('10 seconds', time) AS minute,
                device_id,
                AVG(temperature) AS avg_temperature,
                AVG(humidity) AS avg_humidity,
                AVG(pressure) AS avg_pressure,
                MAX(light) AS max_light,
                MIN(CASE WHEN s0 > 0 THEN s0 END) AS min_s0,
                MIN(CASE WHEN s1 > 0 THEN s1 END) AS min_s1,
                MIN(CASE WHEN s2 > 0 THEN s2 END) AS min_s2,
                MIN(CASE WHEN s3 > 0 THEN s3 END) AS min_s3,
                MIN(CASE WHEN s4 > 0 THEN s4 END) AS min_s4,
                MIN(CASE WHEN s5 > 0 THEN s5 END) AS min_s5,
                MIN(CASE WHEN s6 > 0 THEN s6 END) AS min_s6,
                MIN(CASE WHEN s7 > 0 THEN s7 END) AS min_s7,
                MIN(CASE WHEN s8 > 0 THEN s8 END) AS min_s8,
                MIN(CASE WHEN s9 > 0 THEN s9 END) AS min_s9
            FROM
                sensor_readings
            WHERE
                device_id IN ({devices_list_str})
                AND time >= '{time_from_str}'
                AND time < '{time_to_str}'
            GROUP BY
                minute,
                device_id
        ) sr
        FULL OUTER JOIN (
            SELECT
                time_bucket('10 seconds', time) AS minute,
                device_id,
                MAX(absent) AS absent,
                MAX(moving) AS moving,
                MAX(stationary) AS stationary,
                MAX(\"both\") AS both,
                MAX(m0) AS m0,
                MAX(m1) AS m1,
                MAX(m2) AS m2,
                MAX(m3) AS m3,
                MAX(m4) AS m4,
                MAX(m5) AS m5,
                MAX(m6) AS m6,
                MAX(m7) AS m7,
                MAX(m8) AS m8,
                MAX(s2) AS s2,
                MAX(s3) AS s3,
                MAX(s4) AS s4,
                MAX(s5) AS s5,
                MAX(s6) AS s6,
                MAX(s7) AS s7,
                MAX(s8) AS s8
            FROM
                radar_readings
            WHERE
                device_id IN ({devices_list_str})
                AND time >= '{time_from_str}'
                AND time < '{time_to_str}'
            GROUP BY
                minute,
                device_id
        ) rr
        ON sr.minute = rr.minute AND sr.device_id = rr.device_id
        ORDER BY
            CASE COALESCE(sr.device_id, rr.device_id)
                {case_order}
            END,
            COALESCE(sr.minute, rr.minute);
        """
    return sql

def export_query_to_minio_chunked(connection_params, query, minio_client, bucket_name, blob_name=None, chunksize=10000):
    """
    Export query results to MinIO as CSV in chunks to handle large datasets

    Parameters:
    connection_params (dict): Database connection parameters
    query (str): SQL query to execute
    minio_client: Initialized MinIO client
    bucket_name (str): Name of the MinIO bucket
    blob_name (str): Name for the blob in MinIO. If None, generates timestamped name
    chunksize (int): Number of rows to process at a time

    Returns:
    str: Name of the created blob
    """
    try:
        # Create direct connection using psycopg2
        conn = psycopg2.connect(**connection_params)

        # Generate blob name if not provided
        if blob_name is None:
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            blob_name = f'query_results_{timestamp}.csv'

        # Create a buffer to store CSV data
        csv_buffer = io.StringIO()

        # Stream the query results in chunks
        first_chunk = True
        for chunk_df in pd.read_sql_query(query, conn, chunksize=chunksize):
            # Write header only for the first chunk
            chunk_df.to_csv(
                csv_buffer,
                index=False,
                header=first_chunk,
                mode='a'
            )
            first_chunk = False

        # Get the CSV data as bytes
        csv_buffer.seek(0)
        csv_bytes = csv_buffer.getvalue().encode('utf-8')

        # Upload to MinIO
        minio_client.put_object(
            bucket_name,
            blob_name,
            io.BytesIO(csv_bytes),
            len(csv_bytes)
        )

        print(f"Data exported successfully to MinIO: {bucket_name}/{blob_name}")
        return blob_name

    except Exception as e:
        print(f"Error exporting data: {str(e)}")
        print(f"Traceback: {traceback.format_exc()}")
        raise

    finally:
        if 'conn' in locals():
            conn.close()
        if 'csv_buffer' in locals():
            csv_buffer.close()

def export_query_to_csv_pandas(connection_params, query, output_path=None):
    """
    Export query results to CSV using pandas with psycopg2 connection

    Parameters:
    connection_params (dict): Database connection parameters
    query (str): SQL query to execute
    output_path (str): Path for output CSV file. If None, generates timestamped filename

    Returns:
    str: Path to the created CSV file
    """
    try:
        # Create direct connection using psycopg2
        conn = psycopg2.connect(**connection_params)

        # Generate output path if not provided
        if output_path is None:
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            output_path = f'query_results_{timestamp}.csv'

        # Read query directly into DataFrame using the psycopg2 connection
        df = pd.read_sql_query(query, conn)

        # Export to CSV with all headers
        df.to_csv(output_path, index=False)
        print(f"Data exported successfully to {output_path}")
        return output_path

    except Exception as e:
        print(f"Error exporting data: {str(e)}")
        raise

    finally:
        if 'conn' in locals():
            conn.close()


def CreateDailyCSV(csv_file, devices_list, selected_date, vocs_scaled, time_zone_s, radar_part, consolidated_by, temp_offset):
    global Id2MACDict
    if radar_part == "s28":
        radar_part = "(s2+s3+s4+s5+s6+s7+s8)/7"
    try:

        #stretch_to_min_max = True

        #current_date_p = selected_date.replace("-", "_")
        #current_date_s = selected_date

        lower_than200 = 0
        larger_than200 = 0
        ids_list = []
        for details in devices_list[0]:
            dev_id =  details[0]
            ids_list.append(details[1])
            if dev_id < 200:
                lower_than200 += 1
            else:
                larger_than200 += 1

        if lower_than200 > 0 and larger_than200 > 0:
            return ""

        if larger_than200 > 0:
            sensors_c = len(s_table)
        else: #old sensors not supported
            return ""

        devices_c = len(devices_list[0])
        devices_list_str = ",".join(map(str, devices_list[1]))
        time_from_str, time_to_str = GetLocalTimeForDate(selected_date, time_zone_s)
        if consolidated_by == "by_minute_rc":
            sql = get_deployment_query(devices_list_str, time_from_str, time_to_str, ids_list, radar_part, temp_offset)
        elif consolidated_by == "by_deca_rd":
            sql = get_deployment_deca_query(devices_list_str, time_from_str, time_to_str, ids_list, temp_offset)
        elif consolidated_by == "by_minute_rd":
            sql = get_deployment_rd_query(devices_list_str, time_from_str, time_to_str, ids_list, temp_offset)

        print(sql)

        connection_params = {
            'host': DB_HOST,
            'database': DB_NAME,
            'user': DB_USER,
            'password': DB_PASSWORD,
            'port': DB_PORT
        }

        # Using pandas approach (recommended)
        output_file = export_query_to_minio_chunked(
            connection_params,
            sql,
            miniIO_blob_client,
            "data-downloads",
            csv_file,
            chunksize=10000
        )
        return output_file
    except Exception as e:
        logging.error(str(traceback.format_exc()))
        return ""

def GetBlob(file_name, bucket_name="daily-maps"):
    """
    Retrieve image from blob storage

    Args:
        file_name (str): Name of the file to retrieve from blob storage

    Returns:
        tuple: (image_bytes, content_type)
        Returns None, None if image not found or error occurs
    """
    logger.debug(f"GetBlob({file_name})")
    try:
        # Get the object from blob storage
        data = miniIO_blob_client.get_object(
            bucket_name,
            file_name
        )

        # Read the data into bytes
        data_bytes = data.read()
        #logger.debug(f"len(data_bytes)={len(data_bytes)}")

        if bucket_name == "daily-maps":
            return data_bytes, 'image/png'
        else:
            return data_bytes, 'application/zip'

    except Exception as e:
        logger.error(f"Error: {traceback.format_exc()}")
        return None, None


def MapFileToDate(map_file):
    #'/Volumes/XTRM-Q/wellnuo/daily_maps/1/1_2023-11-07_dayly_image.png'
    parts = map_file.split("/")
    parts = parts[-1].split("_")

    if "-" in parts[0]:
        date_string = parts[0]
    elif "-" in parts[1]:
        date_string = parts[1]

    date_object = datetime.datetime.strptime(date_string, "%Y-%m-%d")
    date_only = date_object.date()
    return date_only

def CSVFileToDate(csv_file):
    #'/Volumes/XTRM-Q/wellnuo/daily_maps/1/1_2023-11-07_dayly_image.png'
    parts = csv_file.split("/")
    parts = parts[-1].split("_")

    if "-" in parts[0]:
        date_string = parts[0]
    elif "-" in parts[1]:
        date_string = parts[1]

    date_object = datetime.datetime.strptime(date_string, "%Y-%m-%d")
    date_only = date_object.date()
    return date_only

def GetMACsListSimple(list_of_lists):

    result = []
    if len(list_of_lists) > 0:
        result = [sublist[3] for sublist in list_of_lists]

    return(result)

def datetime_handler(obj):
    """Handle datetime serialization for JSON"""
    if isinstance(obj, datetime.datetime):
        if obj.tzinfo:
            return obj.isoformat()
        return obj.strftime('%Y-%m-%d %H:%M:%S.%f')
    raise TypeError(f"Object of type {type(obj)} is not JSON serializable")

def ReadCandles(file, sensor, period, time_from, time_to):
    result = []
    if sensor == "voc0":
        sqlr = "SELECT * from vocs_0"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to)
    elif sensor == "voc1":
        sqlr = "SELECT * from vocs_1"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to)
    elif sensor == "voc2":
        sqlr = "SELECT * from vocs_2"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to)
    elif sensor == "voc3":
        sqlr = "SELECT * from vocs_3"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to)
    elif sensor == "voc4":
        sqlr = "SELECT * from vocs_4"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to)
    elif sensor == "voc5":
        sqlr = "SELECT * from vocs_5"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to)
    elif sensor == "voc6":
        sqlr = "SELECT * from vocs_6"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to)
    elif sensor == "voc7":
        sqlr = "SELECT * from vocs_7"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to)
    elif sensor == "voc8":
        sqlr = "SELECT * from vocs_8"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to)
    elif sensor == "voc9":
        sqlr = "SELECT * from vocs_9"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to)
    else:
        sqlr = "SELECT * from "+sensor+"s"+period+ " WHERE Date >= "+str(time_from) + " AND Date <= "+str(time_to)
    logger.debug(f"sqlr = {sqlr}")

    with get_db_connection() as conn:
        with conn.cursor() as cur:
            devices_string = ReadCleanStringDB(cur, sqlr)
        result = QuerrySql(file, sqlr)
    return result

def ReadSensor(device_id, sensor, time_from_epoch, time_to_epoch, data_type, radar_part, bucket_size="no"):

    time_utc = datetime.datetime.fromtimestamp(float(time_from_epoch), tz=timezone.utc)
    # Format in ISO 8601 format with timezone
    time_from_str = time_utc.strftime("%Y-%m-%d %H:%M:%S%z")

    time_utc = datetime.datetime.fromtimestamp(float(time_to_epoch), tz=timezone.utc)
    # Format in ISO 8601 format with timezone
    time_to_str = time_utc.strftime("%Y-%m-%d %H:%M:%S%z")

    legal_min, legal_max, window = sensor_legal_values[sensor]

    result = []
    if sensor == "radar":
        if radar_part == "s28":
            radar_part = "(s2+s3+s4+s5+s6+s7+s8)/7"
        sqlr = f"SELECT time, {radar_part} AS radar FROM radar_readings WHERE device_id = {device_id} AND time >= '{time_from_str}' AND time <= '{time_to_str}' ORDER BY time ASC"
    elif sensor[0] == "s":
        sqlr = f"SELECT time, {sensor} AS smell FROM sensor_readings WHERE device_id = {device_id} AND {sensor} >= '{legal_min}' AND {sensor} <= '{legal_max}' AND time >= '{time_from_str}' AND time <= '{time_to_str}' ORDER BY time ASC"
    else:
        if sensor == "temperature":
            sqlr = f"SELECT time, {sensor} - 16 from sensor_readings WHERE device_id = {device_id} AND {sensor} >= '{legal_min}' AND {sensor} <= '{legal_max}'  AND time >= '{time_from_str}' AND time <= '{time_to_str}' ORDER BY time ASC"
        else:
            sqlr = f"SELECT time, {sensor} from sensor_readings WHERE device_id = {device_id} AND {sensor} >= '{legal_min}' AND {sensor} <= '{legal_max}'  AND time >= '{time_from_str}' AND time <= '{time_to_str}' ORDER BY time ASC"

    logger.debug(f"sqlr = {sqlr}")
    with get_db_connection() as conn:
        with conn.cursor() as cur:
            cur.execute(sqlr)
            result = cur.fetchall()

    return result

def ReadSensor3(device_id, sensor, time_from_epoch, time_to_epoch, data_type, radar_part, bucket_size="no"):
    import datetime
    from datetime import timezone

    # Convert epoch to datetime and format as ISO 8601 strings with timezone
    time_utc = datetime.datetime.fromtimestamp(float(time_from_epoch), tz=timezone.utc)
    time_from_str = time_utc.strftime("%Y-%m-%d %H:%M:%S%z")
    time_utc = datetime.datetime.fromtimestamp(float(time_to_epoch), tz=timezone.utc)
    time_to_str = time_utc.strftime("%Y-%m-%d %H:%M:%S%z")

    legal_min, legal_max, window = sensor_legal_values[sensor]

    # If bucket_size is provided (i.e. not "no"), then use time bucketing.
    use_bucket = bucket_size != "no"
    if use_bucket:
        # Map the shorthand bucket sizes to PostgreSQL interval strings.
        mapping = {
            "10s": "10 seconds",
            "1m": "1 minute",
            "5m": "5 minutes",
            "10m": "10 minutes",
            "15m": "15 minutes",
            "30m": "30 minutes",
            "1h": "1 hour"
        }
        bucket_interval = mapping.get(bucket_size, bucket_size)

    avgmax = "AVG"
    # Build the SQL query based on sensor type.
    if sensor == "radar":
        # For radar sensors, a special aggregation is needed.
        avgmax = "MAX"
        if radar_part == "s28":
            radar_expr = "(s2+s3+s4+s5+s6+s7+s8)/7"
        else:
            radar_expr = radar_part

        if use_bucket:
            sqlr = f"""
                SELECT time_bucket('{bucket_interval}', time) AS time,
                       {avgmax}({radar_expr}) AS radar
                  FROM radar_readings
                 WHERE device_id = {device_id}
                   AND time >= '{time_from_str}'
                   AND time <= '{time_to_str}'
              GROUP BY time_bucket('{bucket_interval}', time)
                 ORDER BY time ASC;
            """
        else:
            sqlr = f"""
                SELECT time, {radar_expr} AS radar
                  FROM radar_readings
                 WHERE device_id = {device_id}
                   AND time >= '{time_from_str}'
                   AND time <= '{time_to_str}'
                 ORDER BY time ASC;
            """
    elif sensor[0] == "s":
        # For sensors whose name starts with "s" (for example, smell sensors)
        if use_bucket:
            sqlr = f"""
                SELECT time_bucket('{bucket_interval}', time) AS time,
                       {avgmax}({sensor}) AS smell
                  FROM sensor_readings
                 WHERE device_id = {device_id}
                   AND {sensor} >= '{legal_min}'
                   AND {sensor} <= '{legal_max}'
                   AND time >= '{time_from_str}'
                   AND time <= '{time_to_str}'
              GROUP BY time_bucket('{bucket_interval}', time)
                 ORDER BY time ASC;
            """
        else:
            sqlr = f"""
                SELECT time, {sensor} AS smell
                  FROM sensor_readings
                 WHERE device_id = {device_id}
                   AND {sensor} >= '{legal_min}'
                   AND {sensor} <= '{legal_max}'
                   AND time >= '{time_from_str}'
                   AND time <= '{time_to_str}'
                 ORDER BY time ASC;
            """
    elif sensor == "co2":
        alias = sensor
        sensor = "s4"
        sqlr = f"""
        WITH s4_values AS (
        SELECT s4
        FROM public.sensor_readings
        WHERE device_id = 559
        AND s4 IS NOT NULL
        ),
        s4_percentile AS (
            SELECT percentile_cont(0.25) WITHIN GROUP (ORDER BY s4 DESC) AS s4_25_percentile
            FROM s4_values
        )
        SELECT s4_25_percentile
        FROM s4_percentile;
        """
        co2_max =  22536000#102400000
        co2_min =  2400000#16825674 #387585

        real_co2_max =  2000
        real_co2_min =  430


        #logger.debug(f"sqlr = {sqlr}")
        #with get_db_connection() as conn:
            #with conn.cursor() as cur:
                #cur.execute(sqlr)
                #result = cur.fetchall()
                #co2_max =  result[0][0]
                #co2_min =  result[0][1]
        #=E17+E20*(102400000-A24)/B18
        #col_expr = f"{real_co2_min}+({real_co2_max}-{real_co2_min})*(102400000-{sensor})/({co2_min}-{co2_max})"
        col_expr = f"GREATEST({real_co2_min},{real_co2_min}+({real_co2_max}-{real_co2_min})*({co2_max}-percentile_cont(0.5) WITHIN GROUP (ORDER BY {sensor}))/({co2_max}-{co2_min}))"

        if use_bucket:
            sqlr = f"""
                SELECT time_bucket('{bucket_interval}', time) AS time,
                       ({col_expr}) AS {alias}
                  FROM sensor_readings
                 WHERE device_id = {device_id}
                   AND {sensor} >= '{legal_min}'
                   AND {sensor} <= '{legal_max}'
                   AND time >= '{time_from_str}'
                   AND time <= '{time_to_str}'
              GROUP BY time_bucket('{bucket_interval}', time)
                 ORDER BY time ASC;
            """
        else:
            sqlr = f"""
                SELECT time, {col_expr} AS {alias}
                  FROM sensor_readings
                 WHERE device_id = {device_id}
                   AND {sensor} >= '{legal_min}'
                   AND {sensor} <= '{legal_max}'
                   AND time >= '{time_from_str}'
                   AND time <= '{time_to_str}'
                 ORDER BY time ASC;
            """
    elif sensor == "voc":
        sensor = "s9"
        alias = sensor
        col_expr = f"{sensor} - 0"

        if use_bucket:
            sqlr = f"""
                SELECT time_bucket('{bucket_interval}', time) AS time,
                       {avgmax}({col_expr}) AS {alias}
                  FROM sensor_readings
                 WHERE device_id = {device_id}
                   AND {sensor} >= '{legal_min}'
                   AND {sensor} <= '{legal_max}'
                   AND time >= '{time_from_str}'
                   AND time <= '{time_to_str}'
              GROUP BY time_bucket('{bucket_interval}', time)
                 ORDER BY time ASC;
            """
        else:
            sqlr = f"""
                SELECT time, {col_expr} AS {alias}
                  FROM sensor_readings
                 WHERE device_id = {device_id}
                   AND {sensor} >= '{legal_min}'
                   AND {sensor} <= '{legal_max}'
                   AND time >= '{time_from_str}'
                   AND time <= '{time_to_str}'
                 ORDER BY time ASC;
            """
    elif sensor == "humidity":
        alias = sensor
        col_expr = f"2.3592 * {sensor} + 32.5546" #= 2.3592 * J2 + 33.5546

        if use_bucket:
            sqlr = f"""
                SELECT time_bucket('{bucket_interval}', time) AS time,
                       {avgmax}({col_expr}) AS {alias}
                  FROM sensor_readings
                 WHERE device_id = {device_id}
                   AND {sensor} >= '{legal_min}'
                   AND {sensor} <= '{legal_max}'
                   AND time >= '{time_from_str}'
                   AND time <= '{time_to_str}'
              GROUP BY time_bucket('{bucket_interval}', time)
                 ORDER BY time ASC;
            """
        else:
            sqlr = f"""
                SELECT time, {col_expr} AS {alias}
                  FROM sensor_readings
                 WHERE device_id = {device_id}
                   AND {sensor} >= '{legal_min}'
                   AND {sensor} <= '{legal_max}'
                   AND time >= '{time_from_str}'
                   AND time <= '{time_to_str}'
                 ORDER BY time ASC;
            """
    else:
        # For other sensors (including temperature, which requires a subtraction)
        alias = sensor
        col_expr = sensor
        if sensor == "temperature":
            col_expr = f"{sensor} - 16"
            alias = "temperature"
        elif sensor == "light":
            avgmax = "MAX"

        if use_bucket:
            sqlr = f"""
                SELECT time_bucket('{bucket_interval}', time) AS time,
                       {avgmax}({col_expr}) AS {alias}
                  FROM sensor_readings
                 WHERE device_id = {device_id}
                   AND {sensor} >= '{legal_min}'
                   AND {sensor} <= '{legal_max}'
                   AND time >= '{time_from_str}'
                   AND time <= '{time_to_str}'
              GROUP BY time_bucket('{bucket_interval}', time)
                 ORDER BY time ASC;
            """
        else:
            sqlr = f"""
                SELECT time, {col_expr} AS {alias}
                  FROM sensor_readings
                 WHERE device_id = {device_id}
                   AND {sensor} >= '{legal_min}'
                   AND {sensor} <= '{legal_max}'
                   AND time >= '{time_from_str}'
                   AND time <= '{time_to_str}'
                 ORDER BY time ASC;
            """

    logger.debug(f"sqlr = {sqlr}")
    with get_db_connection() as conn:
        with conn.cursor() as cur:
            cur.execute(sqlr)
            result = cur.fetchall()

    return result

def ReadRadarDetail(device_id, sensor, time_from_epoch, time_to_epoch, alt_key_state):

    time_utc = datetime.datetime.fromtimestamp(float(time_from_epoch), tz=timezone.utc)
    # Format in ISO 8601 format with timezone
    time_from_str = time_utc.strftime("%Y-%m-%d %H:%M:%S%z")

    time_utc = datetime.datetime.fromtimestamp(float(time_to_epoch), tz=timezone.utc)
    # Format in ISO 8601 format with timezone
    time_to_str = time_utc.strftime("%Y-%m-%d %H:%M:%S%z")
    #sensor_index = int(sensor_index)


    result = []

    #time_period_sec can be "10" (RAW) or "60"
    if alt_key_state == "1": #"RAW = 10 sec"
        radar_part = sensor
        if sensor == "m08_max":
            radar_part = "(m0+m1+m2+m3+m4+m5+m6+m7+m8)/9"
        elif sensor == "s28_max" or sensor == "s28_min":
            radar_part = "(s2+s3+s4+s5+s6+s7+s8)/7"
        sqlr = f"SELECT time, {radar_part} AS radar FROM radar_readings WHERE device_id = {device_id} AND time >= '{time_from_str}' AND time <= '{time_to_str}' ORDER BY time ASC"
    else:
        if sensor == "m08_max":
            radar_part = "MAX((m0+m1+m2+m3+m4+m5+m6+m7+m8)/7) AS m08_max"
        elif sensor == "s28_max":
            radar_part = f"MAX((s2+s3+s4+s5+s6+s7+s8)/7) AS {sensor}"
        elif sensor == "s28_min":
            radar_part = f"MIN((s2+s3+s4+s5+s6+s7+s8)/7) AS {sensor}"
        else:
            radar_part = f"MAX({sensor}) AS {sensor}"

        sqlr = f"""
        SELECT
            minute,
            {sensor} as {sensor}
        FROM (
            SELECT
                time_bucket('1 minute', time) AS minute,
                {radar_part}
            FROM
                radar_readings
            WHERE
                    device_id = {device_id}
                    AND time >= '{time_from_str}'
                    AND time < '{time_to_str}'
                GROUP BY
                    minute
            ) rr

            ORDER BY
                minute
            """
    logger.debug(f"sqlr = {sqlr}")
    with get_db_connection() as conn:
        with conn.cursor() as cur:
            cur.execute(sqlr)
            result = cur.fetchall()

    return result

def check_and_parse(data_str):
    # Remove whitespace to handle cases with spaces
    cleaned = data_str.strip()
    # Check if second character is '['
    is_list_of_lists = cleaned[1] == '['
    if cleaned[0] == '[':
        # Parse the string regardless of type
        parsed = json.loads(cleaned)
    else:
        parsed = cleaned.split(",")
    return is_list_of_lists, parsed

def clean_data_with_rolling_spline(line_part_t, window=5, threshold=2.0):
    """
    Filter outliers using rolling median and replace with spline interpolation
    Returns data in the same format as input: [(timestamp, value), ...]
    """
    # Unzip the input tuples
    x, y = zip(*line_part_t)
    x = np.array(x)
    y = np.array(y, dtype=float)  # explicitly convert to float

    # Calculate rolling median and MAD using a safer approach
    rolling_median = []
    rolling_mad = []

    for i in range(len(y)):
        start_idx = max(0, i - window//2)
        end_idx = min(len(y), i + window//2 + 1)
        window_values = y[start_idx:end_idx]

        # Skip if window is empty or contains invalid values
        if len(window_values) == 0 or np.any(np.isnan(window_values)):
            rolling_median.append(y[i])
            rolling_mad.append(0)
            continue

        med = np.median(window_values)
        mad = np.median(np.abs(window_values - med))

        rolling_median.append(med)
        rolling_mad.append(mad)

    rolling_median = np.array(rolling_median)
    rolling_mad = np.array(rolling_mad)

    # Identify outliers (protect against division by zero)
    outlier_mask = np.abs(y - rolling_median) > threshold * (rolling_mad + 1e-10)
    good_data_mask = ~outlier_mask

    if np.sum(good_data_mask) < 4:
        return line_part_t  # return original data if we can't interpolate

    try:
        # Create and apply spline
        spline = interpolate.InterpolatedUnivariateSpline(
            x[good_data_mask],
            y[good_data_mask],
            k=3
        )

        y_cleaned = y.copy()
        y_cleaned[outlier_mask] = spline(x[outlier_mask])
    except Exception as e:
        print(f"Spline interpolation failed: {e}")
        return line_part_t

    # Return in the same format as input
    return list(zip(x, y_cleaned))

def DatesSpan(date_from: str, date_to: str) -> list:
    """
    Generate a list of dates between date_from and date_to (inclusive).
    Handles cases where date_from is later than date_to.

    Args:
        date_from (str): Start date in 'YYYY-MM-DD' format
        date_to (str): End date in 'YYYY-MM-DD' format

    Returns:
        list: List of dates in 'YYYY-MM-DD' format
    """
    # Convert string dates to datetime objects
    start_date = datetime.datetime.strptime(date_from, '%Y-%m-%d')
    end_date = datetime.datetime.strptime(date_to, '%Y-%m-%d')

    # Determine direction and swap dates if necessary
    if start_date > end_date:
        start_date, end_date = end_date, start_date

    # Generate list of dates
    dates_list = []
    current_date = start_date

    while current_date <= end_date:
        dates_list.append(current_date.strftime('%Y-%m-%d'))
        current_date += timedelta(days=1)

    # Reverse the list if original date_from was later than date_to
    #if datetime.datetime.strptime(date_from, '%Y-%m-%d') > datetime.datetime.strptime(date_to, '%Y-%m-%d'):
    #    dates_list.reverse()

    return dates_list

def zip_blobs(blob_paths, zip_blob_name, bucket_name, minio_client=None):
    """
    Zip multiple blobs from MinIO storage into a single zip file without saving locally.

    Args:
        blob_paths (list): List of blob paths to zip
        zip_blob_name (str): Name/path for the output zip file in MinIO
        bucket_name (str): MinIO bucket name
        minio_client (Minio, optional): Existing MinIO client instance

    Returns:
        bool: True if successful, False otherwise
    """
    try:
        # Create zip file in memory
        zip_buffer = BytesIO()

        with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
            # Process each blob
            for blob_path in blob_paths:
                # Get file name from path for zip entry
                file_name = blob_path.split('/')[-1]

                # Get blob data into memory
                data = minio_client.get_object(bucket_name, blob_path.lstrip('/'))

                # Add file to zip
                zip_file.writestr(file_name, data.read())

                # Close the object to free memory
                data.close()

        # Seek to start of zip file
        zip_buffer.seek(0)

        # Upload zip file to MinIO
        minio_client.put_object(
            bucket_name,
            zip_blob_name.lstrip('/'),
            zip_buffer,
            length=zip_buffer.getbuffer().nbytes
        )

        return True

    except Exception as e:
        print(f"Error creating zip file: {str(e)}")
        return False
    finally:
        # Clean up
        zip_buffer.close()

def clean_data_with_spline(x, y, threshold=2.0):
    """
    Filter outliers and replace with spline interpolation

    Parameters:
    x : array-like, timestamps or x-coordinates
    y : array-like, values to be filtered
    threshold : float, number of median absolute deviations for outlier detection

    Returns:
    array-like : cleaned data with outliers replaced by spline interpolation
    """
    # Convert inputs to numpy arrays
    x = np.array(x)
    y = np.array(y)

    # Calculate median and median absolute deviation
    median = np.median(y)
    mad = stats.median_abs_deviation(y)

    # Identify outliers
    outlier_mask = np.abs(y - median) > threshold * mad
    good_data_mask = ~outlier_mask

    # If we have too few good points for interpolation, adjust threshold
    min_points_needed = 4  # minimum points needed for cubic spline
    if np.sum(good_data_mask) < min_points_needed:
        return y  # return original data if we can't interpolate

    # Create spline with non-outlier data
    spline = interpolate.InterpolatedUnivariateSpline(
        x[good_data_mask],
        y[good_data_mask],
        k=3  # cubic spline
    )

    # Replace outliers with interpolated values
    y_cleaned = y.copy()
    y_cleaned[outlier_mask] = spline(x[outlier_mask])

    return y_cleaned

def clean_data(line_part_t, window=5, threshold=2.0):
    """
    Remove obvious outliers based on window comparison
    Returns cleaned data in the same format: [(timestamp, value), ...]
    """
    if len(line_part_t) < window:
        return line_part_t

    x, y = zip(*line_part_t)
    x = np.array(x)
    y = np.array(y, dtype=float)

    cleaned_data = []

    for i in range(len(y)):
        # Get window around current point
        start_idx = max(0, i - window//2)
        end_idx = min(len(y), i + window//2 + 1)
        window_values = y[start_idx:end_idx]

        # Calculate median and MAD for the window
        window_median = np.median(window_values)
        deviation = abs(y[i] - window_median)

        # Keep point if it's not too far from window median
        if deviation <= threshold * window_median:
            cleaned_data.append((x[i], y[i]))
        #else:
            #print(window_values)
    return cleaned_data

def clean_data_fast(line_part_t, window=5, threshold=2.0):
    """
    Remove obvious outliers based on window comparison - vectorized version
    Returns cleaned data in the same format: [(timestamp, value), ...]
    """
    if len(line_part_t) < window:
        return line_part_t

    x, y = zip(*line_part_t)
    x = np.array(x)
    y = np.array(y, dtype=float)

    # Calculate rolling median using numpy
    half_window = window // 2
    medians = np.array([
        np.median(y[max(0, i-half_window):min(len(y), i+half_window+1)])
        for i in range(len(y))
    ])

    # Calculate deviations for all points at once
    deviations = np.abs(y - medians)

    # Create mask for good points
    good_points = deviations <= threshold * medians

    # Return filtered data using boolean indexing
    return list(zip(x[good_points], y[good_points]))

def clean_data_pd(line_part_t, window=5, percentile=99):
    """
    Remove obvious outliers based on window comparison - pandas version
    Returns cleaned data in the same format: [(timestamp, value), ...]
    """
    #line_part_t =  line_part_t[2000:2100]
    if len(line_part_t) < window:
        return line_part_t

    x, y = zip(*line_part_t)

    # Create pandas Series and calculate rolling median
    series = pd.Series(y)
    medians = series.rolling(window=window, center=True, min_periods=1).median()

    # Calculate deviations
    deviations = np.abs(series - medians)

    largest_deviations = deviations.nlargest(10)
    #print(largest_deviations)

    # Create mask for good points
    deviation_threshold = np.percentile(deviations, percentile)
    good_points = deviations <= deviation_threshold

    # Convert back to numpy arrays for filtering
    x = np.array(x)
    y = np.array(y)

    # Return filtered data
    return list(zip(x[good_points], y[good_points]))

def CombineStripes(result_filename, stripes_files):
    try:
        # Open the first image to get the width and initialize the height
        first_image = Image.open(stripes_files[0])
        width, height = first_image.size

        # Calculate the total height of the combined image
        total_height = height * len(stripes_files)

        # Create a new blank image with the same width and the calculated height
        result_image = Image.new('RGB', (width, total_height))

        # Paste each image onto the result image vertically
        y_offset = 0
        for file_name in stripes_files:
            image = Image.open(file_name)
            result_image.paste(image, (0, y_offset))
            y_offset += height

        # Save the result image
        result_image.save(result_filename)

        # Return success flag
        return True

    except Exception as e:
        print("Error:", e)
        return False

def FindFirstLocalMinimum(counts, bins):
    """
    Find the first local minimum in a histogram after the main peak and calculate its offset.

    Parameters:
    hist: tuple of (counts, bin_edges) from np.histogram()
        The histogram data to analyze

    Returns:
    tuple: (TR, THR_OFFSET)
        TR: float, the bin value (position) of the first local minimum after the main peak
        THR_OFFSET: int, number of bins between the global maximum and the local minimum
    """

    # Find the main peak (global maximum)
    main_peak_idx = np.argmax(counts)

    # Look for the first local minimum after the main peak
    for i in range(main_peak_idx + 1, len(counts) - 1):
        # Check if current point is less than or equal to both neighbors
        if counts[i] <= counts[i-1] and counts[i] <= counts[i+1]:
            # Calculate the bin center value for TR
            TR = (bins[i] + bins[i+1]) / 2
            # Calculate offset from main peak in number of bins
            THR_OFFSET = (bins[i] + bins[i+1]) / 2 - (bins[main_peak_idx] + bins[main_peak_idx+1]) / 2
            return (bins[main_peak_idx] + bins[main_peak_idx+1]) / 2, TR, THR_OFFSET

    # If no local minimum is found, return None for both values
    return None, None

def process_raw_data(data_tuples):
    """
    Convert list of (timestamp, stationary, motion) tuples to separate arrays

    Parameters:
    -----------
    data_tuples : list of tuples
        Each tuple contains (datetime, stationary_value, motion_value)

    Returns:
    --------
    timestamps : array of datetime
    stationary : array of float
    motion : array of float
    """
    timestamps = np.array([t[0] for t in data_tuples])
    stationary = np.array([t[1] for t in data_tuples])
    motion = np.array([t[2] for t in data_tuples])

    return timestamps, stationary, motion

def rolling_std_fast(arr, window_size):
    """
    Fast calculation of rolling standard deviation using NumPy's stride tricks.

    Parameters:
    -----------
    arr : numpy array
        Input array
    window_size : int
        Size of rolling window

    Returns:
    --------
    numpy array
        Rolling standard deviation
    """
    # Compute rolling sum of squares
    r = np.array(arr, dtype=float)
    r2 = np.array(arr, dtype=float) ** 2

    # Calculate cumulative sums
    cum = np.cumsum(np.insert(r, 0, 0))
    cum2 = np.cumsum(np.insert(r2, 0, 0))

    # Get rolling windows
    x = (cum[window_size:] - cum[:-window_size])
    x2 = (cum2[window_size:] - cum2[:-window_size])

    # Calculate standard deviation
    n = window_size
    return np.sqrt((x2/n) - (x/n) ** 2)

def detect_presence(timestamps, stationary_signal, motion_signal, window_size=100,
                    motion_threshold=5, gmm_components=2):
    """
    Detect presence using both stationary and motion signals with adaptive thresholding.

    Parameters:
    -----------
    timestamps : array-like
        Array of datetime objects
    stationary_signal : array-like
        Time series of stationary signal (0-100)
    motion_signal : array-like
        Time series of motion signal (0-100)
    window_size : int
        Size of rolling window for statistics (used only for temporal smoothing)
    motion_threshold : float
        Threshold for significant motion
    gmm_components : int
        Number of components for Gaussian Mixture Model

    Returns:
    --------
    presence_mask : numpy array
        Boolean array indicating presence
    baseline : float
        Computed baseline for stationary signal
    threshold : float
        Computed threshold for stationary signal
    """

    # Convert inputs to numpy arrays
    stationary_signal = np.array(stationary_signal)
    motion_signal = np.array(motion_signal)

    # 1. Fit Gaussian Mixture Model to stationary signal
    gmm = GaussianMixture(n_components=gmm_components, random_state=42)
    X = stationary_signal.reshape(-1, 1)
    gmm.fit(X)

    # Get the component with lowest mean as baseline
    baseline = min(gmm.means_)[0]

    # 2. Calculate adaptive threshold using GMM components
    components_sorted = sorted(zip(gmm.means_.flatten(), gmm.covariances_.flatten()))
    baseline_std = np.sqrt(components_sorted[0][1])
    threshold = baseline + 3 * baseline_std  # 3 sigma rule

    # 3. Combine motion and stationary detection
    presence_mask = np.zeros(len(stationary_signal), dtype=bool)

    # Vectorized operations instead of loop
    presence_mask = (motion_signal > motion_threshold) | (stationary_signal > threshold)

    # 4. Apply temporal smoothing to reduce false transitions
    smooth_window = min(window_size // 4, 10)  # Smaller window for smoothing
    presence_mask = np.convolve(presence_mask.astype(int),
                                np.ones(smooth_window)/smooth_window,
                               mode='same') > 0.5

    return presence_mask, baseline, threshold

def visualize_detection(timestamps, stationary_signal, motion_signal, presence_mask,
                       baseline, threshold, output_file='presence_detection.png'):
    """
    Visualize the detection results and save to file.

    Parameters:
    -----------
    timestamps : array-like
        Array of datetime objects
    stationary_signal : array-like
        Time series of stationary signal
    motion_signal : array-like
        Time series of motion signal
    presence_mask : array-like
        Boolean array indicating presence
    baseline : float
        Computed baseline for stationary signal
    threshold : float
        Computed threshold for stationary signal
    output_file : str
        Path to save the output PNG file
    """
    plt.figure(figsize=(15, 10))

    # Configure time formatting
    date_formatter = mdates.DateFormatter('%H:%M:%S')

    # Plot signals
    plt.subplot(3, 1, 1)
    plt.plot(timestamps, stationary_signal, label='Stationary Signal')
    plt.axhline(y=baseline, color='g', linestyle='--', label='Baseline')
    plt.axhline(y=threshold, color='r', linestyle='--', label='Threshold')
    plt.gca().xaxis.set_major_formatter(date_formatter)
    plt.legend()
    plt.title('Stationary Signal with Baseline and Threshold')
    plt.grid(True)

    plt.subplot(3, 1, 2)
    plt.plot(timestamps, motion_signal, label='Motion Signal')
    plt.gca().xaxis.set_major_formatter(date_formatter)
    plt.legend()
    plt.title('Motion Signal')
    plt.grid(True)

    plt.subplot(3, 1, 3)
    plt.plot(timestamps, presence_mask, label='Presence Detection')
    plt.gca().xaxis.set_major_formatter(date_formatter)
    plt.ylim(-0.1, 1.1)
    plt.legend()
    plt.title('Presence Detection Result')
    plt.grid(True)

    plt.tight_layout()

    # Save to file and close figure to free memory
    plt.savefig(output_file, dpi=300, bbox_inches='tight')
    plt.close()

def FindZeroIntersection(counts, bins, save_plot, device_id):
    """
    Find the zero intersection point by fitting a parabola to the descending slope
    between 50% and 10% of the maximum peak height. Also returns the peak position.

    Parameters:
    counts: array-like
        The histogram counts
    bins: array-like
        The histogram bin edges
    save_plot: str or None, optional
        If provided, saves the visualization to the specified file path

    Returns:
    tuple: (zero_intersections, peak_position)
        zero_intersections: list of floats, x-coordinates where parabola intersects y=0
        peak_position: float, x-coordinate of the histogram maximum peak
    """
    # Find the main peak
    main_peak_idx = np.argmax(counts)
    peak_height = counts[main_peak_idx]
    peak_position = (bins[main_peak_idx] + bins[main_peak_idx+1]) / 2

    # Calculate 75% and 25% of peak height
    height_50 = 0.50 * peak_height
    height_10 = 0.10 * peak_height

    # Find indices where counts cross these thresholds after the peak
    idx_50 = main_peak_idx
    idx_10 = main_peak_idx

    for i in range(main_peak_idx, len(counts)):
        if counts[i] <= height_50 and idx_50 == main_peak_idx:
            idx_50 = i
        if counts[i] <= height_10:
            idx_10 = i
            break

    # If we couldn't find valid points, return None
    if idx_50 == main_peak_idx or idx_10 == main_peak_idx:
        return None, peak_position

    # Get x and y coordinates for fitting
    # Use bin centers for x coordinates
    x_points = np.array([(bins[i] + bins[i+1])/2 for i in range(idx_50, idx_10+1)])
    y_points = counts[idx_50:idx_10+1]

    # Define quadratic function for fitting
    def quadratic(x, a, b, c):
        return a*x**2 + b*x + c

    try:
        popt, pcov = curve_fit(quadratic, x_points, y_points)
        a, b, c = popt

        # Find zeros using quadratic formula
        if a != 0:
            discriminant = b**2 - 4*a*c
            if discriminant >= 0:
                x1 = (-b + np.sqrt(discriminant)) / (2*a)
                x2 = (-b - np.sqrt(discriminant)) / (2*a)
                zero_intersections = sorted([x1, x2])
                # Filter zeros to only include those after the peak
                zero_intersections = [x for x in zero_intersections if x > peak_position]
            else:
                zero_intersections = []
        else:
            # If a ≈ 0, fallback to linear solution
            if b != 0:
                zero_intersections = [-c/b]
            else:
                zero_intersections = []

        if save_plot:
            plt.figure(figsize=(10, 6))

            # Plot histogram
            bin_centers = [(bins[i] + bins[i+1])/2 for i in range(len(counts))]
            plt.bar(bin_centers, counts, width=bins[1]-bins[0], alpha=0.6,
                    color='skyblue', label='Histogram')

            # Plot peak height lines
            plt.axhline(y=height_50, color='g', linestyle='--', alpha=0.5,
                       label='50% Peak Height')
            plt.axhline(y=height_10, color='r', linestyle='--', alpha=0.5,
                       label='10% Peak Height')

            # Plot fitted parabola
            x_fit = np.linspace(min(x_points), max(x_points), 100)
            y_fit = quadratic(x_fit, a, b, c)
            plt.plot(x_fit, y_fit, 'r-', label='Fitted Parabola')

            # Plot points used for fitting
            plt.plot(x_points, y_points, 'ro', alpha=0.5, label='Fitting Points')

            # Plot zero intersections
            for x_zero in zero_intersections:
                plt.plot(x_zero, 0, 'ko', label='Zero Intersection')

            # Plot peak position
            plt.axvline(x=peak_position, color='purple', linestyle='--', alpha=0.5,
                       label='Peak Position')

            # Add labels and legend
            plt.xlabel('Bin Values')
            plt.ylabel('Counts')
            plt.title(f'Histogram Analysis with Parabolic Fit {device_id}')
            plt.legend()

            # Show zero line
            plt.axhline(y=0, color='k', linestyle='-', alpha=0.2)

            # Add text with intersection and peak values
            text = f'Peak Position: {peak_position:.2f}\n'
            if zero_intersections:
                text += f'Zero Intersection(s): {", ".join([f"{x:.2f}" for x in zero_intersections])}'
            plt.text(0.02, 0.98, text, transform=plt.gca().transAxes,
                    verticalalignment='top',
                    bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))

            # Save plot to file
            plt.savefig(save_plot, dpi=300, bbox_inches='tight')
            plt.close()

        return zero_intersections, peak_position
    except RuntimeError:
        print("Warning: Failed to fit parabola")
        return None, peak_position


def GeneratePresenceHistory(filename, recreate_in, deployment_id, filter_minutes, ddate, to_date, now_date, time_zone_s):

    #maps_dates, proximity = GetDeploymentDatesBoth(deployment_id)
    minutes = 1440
    stripes_files = []

    date1_obj = datetime.datetime.strptime(ddate, '%Y-%m-%d')
    date2_obj = datetime.datetime.strptime(to_date, '%Y-%m-%d')

    start_date = min(date1_obj, date2_obj)
    end_date = max(date1_obj, date2_obj)

    # Generate list of all dates
    maps_dates = [
        (start_date + timedelta(days=x)).strftime('%Y-%m-%d')
        for x in range((end_date - start_date).days + 1)
    ]
    maps_dates.reverse()
    days = len(maps_dates)
    stretch_by = int(1000 / days)
    if stretch_by > 50:
        stretch_by = 50

    #lets use 1000 pixels
    #arr_stretched = np.zeros((int(days*stretch_by), minutes, 3), dtype=np.uint8)  #array to be written as image 3 for RGB channels
    result_image = Image.new('RGB', (minutes, int(days*stretch_by)))


    # Paste each image onto the result image vertically
    y_offset = 0

    for ddate in maps_dates:

        force_recreate = recreate_in

        filename_day = f"/{deployment_id}/{deployment_id}_{ddate}_{filter_minutes}_{stretch_by}_daily_locations.png"

        if not force_recreate:

            file_exists, time_modified_utc = check_file_exists(filename_day)
            if file_exists:
                time_modified_local = time_modified_utc.astimezone(pytz.timezone(time_zone_s))
                file_modified_date = time_modified_local.date()
                file_date = MapFileToDate(filename_day)
                if file_modified_date <= file_date:
                    force_recreate = True
            else:
                force_recreate = True

        if force_recreate:

            timee = LocalDateToUTCEpoch(ddate, time_zone_s)+5 #add so date boundary is avoided
            devices_list, device_ids =  GetProximityList(deployment_id, timee)

            CreateDailyLocationMap(filename_day, devices_list, ddate, filter_minutes, time_zone_s, stretch_by)


        image_bytes, content_type = GetBlob(filename_day)
        if image_bytes != None:
            image_stream = io.BytesIO(image_bytes)
            image = Image.open(image_stream)

            #image = Image.open(file_name)
            result_image.paste(image, (0, y_offset))
            image.close()
            image_stream.close()

        y_offset += stretch_by

    # Save directly to MinIO instead of local file
    success = save_to_minio(result_image, filename, DAILY_MAPS_BUCKET_NAME)
    # Clean up
    result_image.close()
    return success

def AddText(room_image_cv2, x, y, room_name, font_size):
    pil_im = Image.fromarray(room_image_cv2)
    draw = ImageDraw.Draw(pil_im)
    font_path = os.path.join(os.path.dirname(__file__), "fonts", "Poppins-Regular.ttf")
    #print(f"Attempting to load font from: {font_path}")
    try:
        font = ImageFont.truetype(font_path, font_size)  # 12px size
    except:
        logger.error(f"Poppins font not found in {font_path}. Please ensure the font file is in your working directory")
        # Fallback to default font if Poppins is not available
        font = ImageFont.load_default()
    draw.text((x, y), room_name, font=font, fill=(150, 150, 150))  # Black color in RGB
    room_image_cv2 = cv2.cvtColor(np.array(pil_im), cv2.COLOR_RGB2BGR)
    return room_image_cv2

def AddTextList(room_image_cv2, strings_list, font_size):
    pil_im = Image.fromarray(room_image_cv2)
    draw = ImageDraw.Draw(pil_im)
    font_path = os.path.join(os.path.dirname(__file__), "fonts", "Poppins-Regular.ttf")
    try:
        font = ImageFont.truetype(font_path, font_size)  # 12px size
    except:
        logger.error("Poppins font not found. Please ensure the font file is in your working directory")
        # Fallback to default font if Poppins is not available
        font = ImageFont.load_default()

    for x, y, room_name in strings_list:
        draw.text((x, y), room_name, font=font, fill=(150, 150, 150))  # Black color in RGB

    room_image_cv2 = cv2.cvtColor(np.array(pil_im), cv2.COLOR_RGB2BGR)
    return room_image_cv2


def AddRoomData(room_image, room_name, data):

    # Example usage:
    radius = 10
    color_t = data["color"]  # BGR format for red
    color = (color_t[2], color_t[1], color_t[0])
    x_offset = 12

    room_image = AddText(room_image, 13, 20, room_name, 50)
    print(data)
    for present in data["presence"]:
        device_id, minute, duration = present
        #duration = 10
        top_left = (x_offset + minute, 140) #bottom_right = (300, 200)
        bottom_right = (x_offset + minute + duration, 260)
        draw_rounded_rectangle(room_image, top_left, bottom_right, radius, color)
    return room_image

def AddFooterData(image):
    '12, 370, 736, 1092, 1452'
    step_size = 1440 / 4
    string_width = 60
    offset = 12
    yoffset = 30
    step = 0
    font_size = 40
    image = AddText(image, offset+step * step_size + step_size/2 - string_width/2, yoffset, "3 AM", font_size)
    step = 1
    image = AddText(image, offset+step * step_size + step_size/2 - string_width/2, yoffset, "9 AM", font_size)
    step = 2
    image = AddText(image, offset+step * step_size + step_size/2 - string_width/2, yoffset, "3 PM", font_size)
    step = 3
    image = AddText(image, offset+step * step_size + step_size/2 - string_width/2, yoffset, "9 PM", font_size)
    return image

def draw_rounded_rectangle(image, top_left, bottom_right, radius, color):
    """
    Draw a filled rectangle with rounded corners, using simple rectangle for small dimensions
    :param image: Image to draw on
    :param top_left: Top-left corner coordinates (x, y)
    :param bottom_right: Bottom-right corner coordinates (x, y)
    :param radius: Desired corner radius (will be adjusted if needed)
    :param color: Rectangle color in BGR format
    """
    x1, y1 = top_left
    x2, y2 = bottom_right

    # Calculate width
    width = x2 - x1


    # Adjust radius if width or height is too small
    # Maximum radius should be half of the smaller dimension
    max_radius = abs(width) // 2
    radius = min(radius, max_radius)

    # If width is too small, fallback to regular rectangle
    if width <= 4 or radius <= 1:
        cv2.rectangle(image, top_left, bottom_right, color, -1)
        return

    # Adjust radius if needed
    radius = min(radius, width // 2)

    # Create points for the main rectangle
    pts = np.array([
        [x1 + radius, y1],
        [x2 - radius, y1],
        [x2, y1 + radius],
        [x2, y2 - radius],
        [x2 - radius, y2],
        [x1 + radius, y2],
        [x1, y2 - radius],
        [x1, y1 + radius]
    ], np.int32)

    # Fill the main shape
    cv2.fillPoly(image, [pts], color)

    # Fill the corners
    cv2.ellipse(image, (x1 + radius, y1 + radius), (radius, radius), 180, 0, 90, color, -1)
    cv2.ellipse(image, (x2 - radius, y1 + radius), (radius, radius), 270, 0, 90, color, -1)
    cv2.ellipse(image, (x1 + radius, y2 - radius), (radius, radius), 90, 0, 90, color, -1)
    cv2.ellipse(image, (x2 - radius, y2 - radius), (radius, radius), 0, 0, 90, color, -1)

def filter_device(locations_list, device_id):
    result = []
    for entry in locations_list:
        if entry[0] == device_id:
            result.append(entry)

    return result

def GenerateLocationsMap(date_st, devices_list, devices_map, locations_list, time_zone_s):

    devices_list_t = [("date",date_st)]


    for mac in devices_list:
        well_id, device_id, room = devices_map[mac]
        #room = devices[well_id][0]
        color = Loc2Color[room][0]
        presence_data =  filter_device(locations_list, device_id)
        room_details = (room, {"color": color, "presence": presence_data})
        devices_list_t.append(room_details)

    well_id = 0
    device_id = 0
    room = "Outside/?"
    color = (0, 0, 0)

    #lets's not draw future unknown!
    presence_data =  filter_device(locations_list, device_id)
    current_utc = datetime.datetime.now(pytz.UTC)
    current_date_local = current_utc.astimezone(pytz.timezone(time_zone_s))
    current_minute_of_day = current_date_local.hour * 60 + current_date_local.minute

    if date_st == current_date_local.strftime('%Y-%m-%d'):
        filtered_presence_data = []
        for entry in presence_data:
            if entry[1] < current_minute_of_day :
                if entry[1] + entry[2] < current_minute_of_day:
                    filtered_presence_data.append(entry)
                else:
                    entry[2] = (current_minute_of_day - entry[2])
                    if  entry[2] > 0:
                        filtered_presence_data.append(entry)
        #print(presence_data)
    else:
        filtered_presence_data =  presence_data

    room_details = (room, {"color": color, "presence": filtered_presence_data})
    devices_list_t.append(room_details)

    return devices_list_t

def CreateDailyLocationChart(filename_chart_image_day, locations):
    result = False
    header_image_file = "header.png"
    room_image_file = "room.png"
    footer_image_file = "footer.png"

    #ToDo: change it so it reads files from MinIo
    header_image_file = os.path.join(filesDir, header_image_file)
    header_image_file = header_image_file.replace("\\","/")
    header_image = cv2.imread(header_image_file)
    #header_height, header_width = header_image.shape[:2]

    room_image_file = os.path.join(filesDir, room_image_file)
    room_image_file = room_image_file.replace("\\","/")
    room_image = cv2.imread(room_image_file)
    #room_height, room_width = room_image.shape[:2]

    footer_image_file = os.path.join(filesDir, footer_image_file)
    footer_image_file = footer_image_file.replace("\\","/")
    footer_image = cv2.imread(footer_image_file)

    all_images = [header_image]
    for item_c in locations:
        item = item_c[0]
        if item == "date":
            date = item_c[1]
        else:
            room_image = cv2.imread(room_image_file)
            data = item_c[1]
            room_image = AddRoomData(room_image, item, data)
            all_images.append(room_image)

    footer_image = AddFooterData(footer_image)
    all_images.append(footer_image)
    final_image = np.vstack(all_images)
    #this needs to write straight to MinIo !
    SaveImageInBlob(filename_chart_image_day, final_image)
    result = True
    #cv2.imwrite(filename_chart_image_day, final_image)
    #print(rooms_count)
    return result


def GetOptimumFontSize(target_width, text="00", min_size=1, max_size=100, tolerance=1):
    """
    Find optimal font size to fit text within target width using binary search.

    Args:
        target_width (int): Desired width in pixels
        text (str): Text to measure (default "00")
        min_size (int): Minimum font size to try
        max_size (int): Maximum font size to try
        tolerance (int): Acceptable difference from target width

    Returns:
        int: Optimal font size
    """
    while min_size <= max_size:
        current_size = (min_size + max_size) // 2
        width, _ = GetStringSize(text, current_size)

        if abs(width - target_width) <= tolerance:
            return current_size
        elif width > target_width:
            max_size = current_size - 1
        else:
            min_size = current_size + 1

    # Return the largest size that fits within target width
    width, _ = GetStringSize(text, min_size)
    return min_size if width <= target_width else min_size - 1

def GetStringSize(some_string, font_size):
    font_path = os.path.join(os.path.dirname(__file__), "fonts", "Poppins-Regular.ttf")
    try:
        font = ImageFont.truetype(font_path, font_size)  # 12px size
    except:
        logger.error("Poppins font not found. Please ensure the font file is in your working directory")
        # Fallback to default font if Poppins is not available
        font = ImageFont.load_default()

    bbox = font.getbbox(some_string)
    return bbox[2] - bbox[0], bbox[3] - bbox[1]

def GeneratePresenceHistoryChart(filename, recreate_in, deployment_id, filter_minutes, ddate, to_date, now_date, time_zone_s):

    #maps_dates, proximity = GetDeploymentDatesBoth(deployment_id)
    minutes = 1440
    stripes_files = []

    date1_obj = datetime.datetime.strptime(ddate, '%Y-%m-%d')
    date2_obj = datetime.datetime.strptime(to_date, '%Y-%m-%d')

    start_date = min(date1_obj, date2_obj)
    end_date = max(date1_obj, date2_obj)

    # Generate list of all dates
    maps_dates = [
        (start_date + timedelta(days=x)).strftime('%Y-%m-%d')
        for x in range((end_date - start_date).days + 1)
    ]
    #maps_dates.reverse()
    days = len(maps_dates)
    #stretch_by = int(1000 / days)
    #if stretch_by > 50:
        #stretch_by = 50
    stretch_by = 30

    #background_image_file = os.path.join(filesDir, "multi_day_template.png")
    background_image_file = os.path.join(filesDir, "multi_day_template2.png")
    background_image_file = background_image_file.replace("\\","/")
    background_image = cv2.imread(background_image_file)

    rgb_image = background_image #cv2.cvtColor(background_image, cv2.COLOR_BGR2RGB)
    result_image = Image.fromarray(rgb_image)    # Convert to PIL Image
    #result_image = Image.new('RGB', (minutes, int(days*stretch_by)))

    # Paste each image onto the result image vertically
    y_offset = 0
    locations_list = []
    font_size = 50
    string_width, string_height = GetStringSize("00", font_size)


    success = False
    if len(maps_dates) == 1:

        filename_chart_image_day = f"/{deployment_id}/{deployment_id}_{ddate}_{filter_minutes}_{stretch_by}_daily_locations_chart.png"
        force_recreate = recreate_in
        #it is faster to resize existing daily location chart (length is always 1440), than having to re-create it each time...
        filename_day = f"/{deployment_id}/{deployment_id}_{ddate}_{filter_minutes}_daily_locations.png"
        filename_chart_data_day = filename_day+".bin"
        if not force_recreate:
            file_exists, time_modified_utc = check_file_exists(filename_chart_image_day)
            if file_exists:
                time_modified_local = time_modified_utc.astimezone(pytz.timezone(time_zone_s))
                time_modified_date = time_modified_local.date()
                file_date = ddate
                if time_modified_date <= file_date:
                    force_recreate = True
            else:
                force_recreate = True

            if not force_recreate:
                file_exists1, time_modified_utc1 = check_file_exists(filename_chart_data_day)
                if file_exists1:
                    time_modified_local = time_modified_utc.astimezone(pytz.timezone(time_zone_s))
                    time_modified_date = time_modified_local.date()
                    file_date = ddate
                    if time_modified_date <= file_date:
                        force_recreate = True
                else:
                    force_recreate = True



        if force_recreate:

            timee = LocalDateToUTCEpoch(ddate, time_zone_s)+5 #add so date boundary is avoided
            devices_list_a, device_ids =  GetProximityList(deployment_id, timee)
            CreateDailyLocationMap(filename_day, devices_list_a, ddate, filter_minutes, time_zone_s, stretch_by)
            locations_list_s = ReadObjectMinIO("daily-maps", filename_chart_data_day)
            locations_list = json.loads(locations_list_s)

            devices_map = {}
            devices_list = []
            for device_entry in devices_list_a:
                devices_map[device_entry[4]] = [device_entry[0], device_entry[1], device_entry[2]]
                devices_list.append(device_entry[4])

            locations = GenerateLocationsMap(ddate, devices_list, devices_map, locations_list, time_zone_s)
            success = CreateDailyLocationChart(filename, locations)
    else:

        day_counter = 0
        day_step_width = int(1780 / days)
        x_offset = 563
        y_offset = 1615
        h_labels_bottom = 1720
        day_width = int(0.9 * day_step_width)
        day_height = 1440
        font_size = GetOptimumFontSize(day_width, "00", 10, 50, 0)
        string_width, string_height = GetStringSize("00", font_size)
        #logger.debug(f"font_size={font_size}  string_width={string_width}")

        y_offset = y_offset - day_height
        filename_chart_image_day = f"/{deployment_id}/{deployment_id}_{ddate}_{filter_minutes}_{stretch_by}_daily_locations_chart.png"

        for ddate in maps_dates:

            force_recreate = recreate_in

            filename_day = f"/{deployment_id}/{deployment_id}_{ddate}_{filter_minutes}_{stretch_by}_daily_locations.png"

            if not force_recreate:
                file_exists, time_modified_utc = check_file_exists(filename_day)
                file_existsS, time_modifiedS_utc = check_file_exists(filename_day[:-4]+"S.png")
                if file_exists and file_existsS:
                    time_modified_local = time_modified_utc.astimezone(pytz.timezone(time_zone_s))
                    time_modified_date = time_modified_local.date()
                    file_date = MapFileToDate(filename_day)
                    if time_modified_date <= file_date:
                        force_recreate = True
                else:
                    force_recreate = True


            if force_recreate:

                timee = LocalDateToUTCEpoch(ddate, time_zone_s)+5 #add so date boundary is avoided
                devices_list, device_ids =  GetProximityList(deployment_id, timee)

                CreateDailyLocationMap(filename_day, devices_list, ddate, filter_minutes, time_zone_s, stretch_by)

            #here we need to rotate and resize to:

            image_bytes, content_type = GetBlob(filename_day)
            image_bytes_s, content_type_s = GetBlob(filename_day[:-4]+"S.png")
            if image_bytes != None:
                image_stream = io.BytesIO(image_bytes)
                image = Image.open(image_stream)
                numpy_image = np.array(image)
                rotated_image = cv2.rotate(numpy_image, cv2.ROTATE_90_COUNTERCLOCKWISE)
                scaled_image = cv2.resize(rotated_image, (day_width, day_height), interpolation=cv2.INTER_AREA)

                # Convert from BGR to RGB
                rgb_image = cv2.cvtColor(scaled_image, cv2.COLOR_BGR2RGB)
                # Convert to PIL Image
                pil_image = Image.fromarray(rgb_image)

                #image = Image.open(file_name)
                x_origin = x_offset + day_step_width * day_counter + int(0.05 * day_step_width)
                result_image.paste(pil_image, (x_origin, y_offset))

                image_stream = io.BytesIO(image_bytes_s)
                image = Image.open(image_stream)
                numpy_image = np.array(image)
                rotated_image = cv2.rotate(numpy_image, cv2.ROTATE_90_COUNTERCLOCKWISE)
                scaled_image = cv2.resize(rotated_image, (day_width, day_height), interpolation=cv2.INTER_AREA)

                # Convert from BGR to RGB
                rgb_image = cv2.cvtColor(scaled_image, cv2.COLOR_BGR2RGB)
                # Convert to PIL Image
                pil_image = Image.fromarray(rgb_image)

                #image = Image.open(file_name)
                x_origin = x_offset + day_step_width * day_counter + int(0.05 * day_step_width)
                result_image.paste(pil_image, (x_origin, 1807+y_offset))


                image.close()
                image_stream.close()

            day_counter += 1

        pil_im = result_image
        #result_image_cv2 = cv2.cvtColor(np.array(pil_im), cv2.COLOR_RGB2BGR)
        result_image_cv2 = np.array(pil_im)#cv2.cvtColor(np.array(pil_im), cv2.COLOR_RGB2BGR)
        strings_list = []
        day_counter = 0
        for ddate in maps_dates:
            if string_width <= day_width:
                date_str = ddate[8:10]
                x_origin = x_offset + int(day_step_width * (day_counter + 0.5)) - int(string_width / 2)
                strings_list.append((x_origin, h_labels_bottom, date_str))
            day_counter += 1
        result_image_cv2 = AddTextList(result_image_cv2, strings_list, font_size)


        #Y 124 to 1636
        labels_bottom = 1636 - 1.5 * string_height
        x_offset = 340
        step = -4 * 60 #4 hours
        font_size = 50
        strings_list = []
        count = 0
        y_offset = labels_bottom + count * step
        strings_list.append((x_offset, y_offset, "12 AM"))
        count = 1
        y_offset = labels_bottom + count * step
        strings_list.append((x_offset, y_offset, "4 AM"))
        count = 2
        y_offset = labels_bottom + count * step
        strings_list.append((x_offset, y_offset, "8 AM"))
        count = 3
        y_offset = labels_bottom + count * step
        strings_list.append((x_offset, y_offset, "12 PM"))
        count = 4
        y_offset = labels_bottom + count * step
        strings_list.append((x_offset, y_offset, "4 PM"))
        count = 5
        y_offset = labels_bottom + count * step
        strings_list.append((x_offset, y_offset, "8 PM"))
        count = 6
        y_offset = labels_bottom + count * step
        strings_list.append((x_offset, y_offset, "12 AM"))

        result_image_cv2 = AddTextList(result_image_cv2, strings_list, font_size)

        numpy_image = np.array(result_image_cv2)

        success = SaveImageInBlob(filename, numpy_image)
    #SaveImageInBlob(filename, result_image)
    # Save directly to MinIO instead of local file
    #if success:
    #    success = save_to_minio(result_image, filename, DAILY_MAPS_BUCKET_NAME)
    # Clean up
    if success:
        return filename
    else:
        return ""

def GeneratePresenceHistoryFiles(filename, recreate_in, deployment_id, filter_minutes, ddate, to_date, now_date, time_zone_s):

    date1_obj = datetime.datetime.strptime(ddate, '%Y-%m-%d')
    date2_obj = datetime.datetime.strptime(to_date, '%Y-%m-%d')

    start_date = min(date1_obj, date2_obj)
    end_date = max(date1_obj, date2_obj)
    stretch_by = 30

    # Generate list of all dates
    maps_dates = [
        (start_date + timedelta(days=x)).strftime('%Y-%m-%d')
        for x in range((end_date - start_date).days + 1)
    ]

    day_counter = 0

    for ddate in maps_dates:

        force_recreate = recreate_in

        filename_day = f"/{deployment_id}/{deployment_id}_{ddate}_{filter_minutes}_{stretch_by}_daily_locations.png"

        if not force_recreate:
            file_exists, time_modified_utc = check_file_exists(filename_day+".bin")
            if file_exists:
                time_modified_local = time_modified_utc.astimezone(pytz.timezone(time_zone_s))
                time_modified_date = time_modified_local.date()
                file_date = MapFileToDate(filename_day)
                if time_modified_date <= file_date:
                    force_recreate = True
            else:
                force_recreate = True


        if force_recreate:

            timee = LocalDateToUTCEpoch(ddate, time_zone_s)+5 #add so date boundary is avoided
            devices_list, device_ids =  GetProximityList(deployment_id, timee)

            CreateDailyLocationMap(filename_day, devices_list, ddate, filter_minutes, time_zone_s, 10)

        day_counter += 1

    return filename

def CalcStdevs(row, stdev_range, stdevs):
    half_range = stdev_range // 2
    data_len = len(row)

    # Calculate standard deviations with proper window alignment
    for i in range(data_len):
        # Calculate window boundaries
        start = max(0, i - half_range)
        end = min(data_len, i + half_range + 1)

        # Get data within window
        window_data = row[start:end]

        # Calculate standard deviation if we have data
        if len(window_data) > 0:
            stdevs[i] = np.std(window_data)

    # Find amplitude (max - min of standard deviations)
    amplitude = np.max(stdevs) - np.min(stdevs)

    # Scale to range 0-1279
    if amplitude > 0:  # Avoid division by zero
        stdevs = ((stdevs - np.min(stdevs)) / amplitude * 1279).astype(np.float32)

    return stdevs, amplitude

def CalcLife(row, stdev_range, stdevs):
    half_range = stdev_range // 2
    data_len = len(row)

    # Calculate standard deviations with proper window alignment
    for i in range(data_len):
        # Calculate window boundaries
        start = max(0, i - half_range)
        end = min(data_len, i + half_range + 1)

        # Get data within window
        window_data = row[start:end]

        # Calculate standard deviation if we have data
        if len(window_data) > 0:
            stdevs[i] = np.std(window_data)

    # Find amplitude (max - min of standard deviations)
    amplitude = np.max(stdevs) - np.min(stdevs)

    # Scale to range 0-1279
    if amplitude > 0:  # Avoid division by zero
        stdevs = ((stdevs - np.min(stdevs)) / amplitude * 1279).astype(np.float32)

    return stdevs, amplitude

def FindCalibrationDate(device_ids, ddate):
    PCD = 50 #% (Peak Contained Data %)
    PHB = 50 #% (Peak Height from Base %)
    MPW = 10 #? (Max Peak Width)
    MPSD =10 #? (Minimum Presence signal Standard Deviation)
    #Find first day with, for all devices:
    #- enough radar data points collected
    #-Single histogram peak containing more than  PCD% of data and peak width (at PHB% height) is < MPW
    #Stdev of Data larger > MPSD
    return ddate

def FindThreshold(data, percent_list):
    """
    Find the threshold value above which lies the specified percentage of points.

    Args:
        data: numpy array of values
        percent: percentage of points that should be above the threshold (0-100)

    Returns:
        threshold value
    """
    percent_from, percent_to = percent_list
    # Sort data in descending order
    sorted_data = np.sort(data)[::-1]

    # Calculate the index corresponding to the desired percentage
    index_from = int((percent_from / 100) * len(data))
    index_to = int((percent_to / 100) * len(data))

    # Return the threshold value
    return sorted_data[index_from], sorted_data[index_to]

def ShowThresholdGraph(data, filename, threshold_low, threshold_high, title, AveragePercentSpendsThere, location):
    """
    Create and save a threshold analysis graph with maximum curvature point.
    """
    dpi=600
    # Get min and max values
    min_val = np.min(data)
    max_val = np.max(data)

    # Create 1000 threshold levels from max to min
    thresholds = np.linspace(max_val, min_val, 1000)
    threshold_percentages = np.linspace(0, 100, 1000)

    # Calculate percentage of points above each threshold
    points_above = []
    total_points = len(data)

    for thresh in thresholds:
        above_count = np.sum(data > thresh)
        percentage = (above_count / total_points) * 100
        points_above.append(percentage)

    points_above = np.array(points_above)

    # Calculate derivatives and smooth them
    first_derivative = np.gradient(points_above)
    second_derivative = np.gradient(first_derivative)

    #first_derivative = savgol_filter(np.gradient(points_above), window_length=51, polyorder=3)
    #second_derivative = savgol_filter(np.gradient(first_derivative), window_length=51, polyorder=3)


    # Find the point of maximum absolute second derivative
    # Exclude edges (first and last 5% of points) to avoid edge effects
    edge_margin = len(second_derivative) // 20  # 5% of points
    valid_range = slice(edge_margin, -edge_margin)
    max_curve_idx = edge_margin + np.argmax(np.abs(second_derivative[valid_range]))

    max_curve_x = threshold_percentages[max_curve_idx]
    max_curve_y = points_above[max_curve_idx]
    max_curve_second_deriv = second_derivative[max_curve_idx]

    # Calculate the actual threshold value for this point
    threshold2 = max_val - (max_curve_x/100) * (max_val - min_val)

    # Create subplot figure
    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10), height_ratios=[3, 2])

    # Plot 1: Original curve with thresholds
    ax1.plot(threshold_percentages, points_above, 'b-', linewidth=2, label='Distribution')
    ax1.grid(True, linestyle='--', alpha=0.7)

    # Add original threshold line if provided
    if threshold_low is not None:
        threshold_percent = ((max_val - threshold_low) / (max_val - min_val)) * 100
        percent_above = (np.sum(data > threshold_low) / total_points) * 100

        ax1.axvline(x=threshold_percent, color='r', linestyle='--', label=f'Minimum % spent in {location}: {AveragePercentSpendsThere[0]:.3f}')
        ax1.axhline(y=percent_above, color='r', linestyle='--')

        #ax1.annotate(f'Threshold 1: {threshold_low:.3f}\nPoints above: {percent_above:.1f}%',
                    #xy=(threshold_percent, percent_above),
                    #xytext=(10, 10), textcoords='offset points',
                    #bbox=dict(boxstyle='round,pad=0.5', fc='yellow', alpha=0.5),
                    #arrowprops=dict(arrowstyle='->'))

    if threshold_high is not None:
        threshold_percent = ((max_val - threshold_high) / (max_val - min_val)) * 100
        percent_above = (np.sum(data > threshold_high) / total_points) * 100

        ax1.axvline(x=threshold_percent, color='b', linestyle='--', label=f'Maximum % spent in {location}: {AveragePercentSpendsThere[1]:.3f}')
        ax1.axhline(y=percent_above, color='b', linestyle='--')

        #ax1.annotate(f'Threshold 1: {threshold_high:.3f}\nPoints above: {percent_above:.1f}%',
                    #xy=(threshold_percent, percent_above),
                    #xytext=(10, 10), textcoords='offset points',
                    #bbox=dict(boxstyle='round,pad=0.5', fc='yellow', alpha=0.5),
                    #arrowprops=dict(arrowstyle='->'))

    # Add maximum curvature point threshold
    ax1.axvline(x=max_curve_x, color='g', linestyle='--', label=f'Threshold 2: {threshold2:.3f}')
    ax1.axhline(y=max_curve_y, color='g', linestyle='--')
    ax1.plot(max_curve_x, max_curve_y, 'go', markersize=10)

    ax1.annotate(f'Threshold 2: {threshold2:.3f}\nPoints above: {max_curve_y:.1f}%',
                xy=(max_curve_x, max_curve_y),
                xytext=(10, -20), textcoords='offset points',
                bbox=dict(boxstyle='round,pad=0.5', fc='lightgreen', alpha=0.5),
                arrowprops=dict(arrowstyle='->'))

    ax1.set_xlabel('Threshold Level (%)\n0% = Maximum, 100% = Minimum')
    ax1.set_ylabel('Points Above Threshold (%)')
    ax1.set_title(title)
    ax1.set_xlim(0, 100)
    ax1.set_ylim(0, 100)
    ax1.legend()

    # Plot 2: Rate of change
    ax2.plot(threshold_percentages, first_derivative, 'g-', label='First derivative', alpha=0.7)
    ax2.plot(threshold_percentages, second_derivative, 'r-', label='Second derivative', alpha=0.7)
    ax2.grid(True, linestyle='--', alpha=0.7)

    # Mark maximum curvature point on derivative plot
    ax2.axvline(x=max_curve_x, color='g', linestyle='--')
    # Plot point exactly on the second derivative curve
    ax2.plot(max_curve_x, max_curve_second_deriv, 'go', markersize=10,
             label=f'Max curvature at {max_curve_x:.1f}%')

    ax2.set_xlabel('Threshold Level (%)')
    ax2.set_ylabel('Rate of Change')
    ax2.set_title('Rate of Change Analysis')
    ax2.legend()

    plt.tight_layout()
    plt.savefig(filename, dpi=dpi, bbox_inches='tight')
    plt.close()

    return threshold2, max_curve_x, max_curve_y

def add_boundary_points(line_part_t, time_zone):
    """
    Add boundary points (00:00:00 and 23:59:59) to a time series list.

    Args:
        line_part_t: List of tuples (timestamp, value)
        time_zone: String representing the timezone (e.g., "America/Los_Angeles")

    Returns:
        List of tuples with added boundary points
    """
    if not line_part_t:
        return line_part_t

    tz = pytz.timezone(time_zone)

    # Get the date from the first point
    first_dt = datetime.datetime.fromtimestamp(line_part_t[0][0], tz)
    date = first_dt.date()

    # Create datetime objects for start and end of the day
    start_dt = tz.localize(datetime.datetime.combine(date, datetime.datetime.min.time()))
    end_dt = tz.localize(datetime.datetime.combine(date, datetime.datetime.max.time()))

    # Convert to timestamps
    start_ts = start_dt.timestamp()
    end_ts = end_dt.timestamp()

    result = list(line_part_t)

    # Handle start point (00:00:00)
    first_point_dt = datetime.datetime.fromtimestamp(line_part_t[0][0], tz)
    time_diff = first_point_dt - start_dt

    start_value = line_part_t[0][1]

    # Add start point at the beginning
    #result.insert(0, (start_ts, start_value))

    # Handle end point (23:59:59)
    last_point_dt = datetime.datetime.fromtimestamp(line_part_t[-1][0], tz)
    end_value = line_part_t[-1][1]
    # Add end point
    result.append((end_ts, end_value))

    return result

def calculate_life_and_average(my_data1, stdev_range=5):
    # Convert data to numpy array for faster operations
    data_array = np.array(my_data1)

    # Calculate half range
    stdev_range_h = stdev_range // 2

    # Pre-calculate indices for the sliding window
    indices = np.arange(len(data_array) - 2 * stdev_range_h)[:, None] + np.arange(2 * stdev_range_h + 1)

    # Get sliding windows of data
    windows = data_array[indices]

    # Calculate average (using column 3)
    average = np.mean(windows[:, :, 3], axis=1)

    # Calculate life (using columns 2, 3, and 4)
    deltas = windows[:, :, 3] - windows[:, :, 2] + windows[:, :, 4]
    life = np.mean(deltas, axis=1)

    return life.tolist(), average.tolist()

def TryJulia(prompt):

    if len(prompt) > 0:

        if prompt[0] == "#":
            return prompt.upper()

        if prompt not in utterances:
            return ""
        else:
            intent = utterances[prompt]
            action = intents[intent]

            return action[0]
    else:
        return ""

def AskGPT(in_prompt, language_from, language_to):

    if len(in_prompt) > 4:

        prompt = in_prompt.lower()

        if language_to.lower() not in language_from.lower():
            prompt = in_prompt + " Answer in " + language_to


        print(prompt)

        #lets see if question is looking for OSM query
        pattern = "what is only the node line for query for * on openstreetmap api? do not answer with url to nominatim, but with query!"


        if match_with_wildcard(prompt, pattern):
            differing_part = extract_differing_part(prompt, pattern)

            if differing_part != "":

                print(differing_part)

                if differing_part in searches_dict:
                    response = searches_dict[differing_part]
                    print(response)
                    return response, language_to
                else:
                    #check if one of synonims:
                    if differing_part in searches_dict["synonims"]:
                        differing_part =  searches_dict["synonims"][differing_part]
                        if differing_part != "":
                            if differing_part in searches_dict[differing_part]:
                                response = searches_dict[differing_part]
                                print(response)
                                return response, language_to

        hash_string = hashlib.sha256(str(prompt).encode('utf-8')).hexdigest()
        #filename=os.path.join(cache_path, "chgpt_query_" + hash_string+".pkl")

        julia_present = False
        if prompt.startswith("julia"):
            prompt = prompt[len("julia") + 1:]
            julia_present = True

        completion = ""
        if julia_present == False:
            completion = TryJulia(prompt)
            #if completion == "":
            #    if os.path.exists(filename):
            #        #completion = pickle.load(open( filename, "rb" ))
            #        completion = (completion.choices[0].message.content.strip(), language_to)[0]
        else:
            completion = TryJulia(prompt)


        if completion == "":

            st = time.time()
            #import wandb

            #run = wandb.init(project='GPT-4 in Python')
            #prediction_table = wandb.Table(columns=["prompt", "prompt tokens", "completion", "completion tokens", "model", "total tokens"])

            print(time.time() - st)

            openai.api_key = OPENAI_API_KEY
            client = OpenAI(
                # This is the default and can be omitted
                api_key = OPENAI_API_KEY
            )

            completion = client.chat.completions.create(
            messages=[
                {
                  "role": "user",
                  "content": prompt,
                }
            ],
            model="gpt-3.5-turbo",
            )


            #with open(filename, 'wb') as handle:
                #pickle.dump(completion, handle, protocol=pickle.HIGHEST_PROTOCOL)

            response = (completion.choices[0].message.content.strip(), language_to)
        else:
            response = (completion, language_to)

    else:
        response = ("question is too short", language_to)


    print(response)
    return response

def AskGPTPure(in_prompt):

    if len(in_prompt) > 4:

        prompt = in_prompt.lower()

        print(prompt)

        st = time.time()

        print(time.time() - st)

        openai.api_key = OPENAI_API_KEY
        client = OpenAI(
            # This is the default and can be omitted
            api_key = OPENAI_API_KEY
        )

        completion = client.chat.completions.create(
        messages=[
            {
              "role": "user",
              "content": prompt,
            }
        ],
        model="gpt-3.5-turbo",
        )

        response = completion.choices[0].message.content.strip()


    else:
        response = "question is too short"


    print(response)
    return response

def get_last_n_days(n=14, timezone_str='America/Los_Angeles'):
    # Get current UTC time
    utc_now = datetime.datetime.now(pytz.UTC)

    # Convert to the specified timezone
    local_now = utc_now.astimezone(pytz.timezone(timezone_str))

    # Get the current date in the specified timezone
    current_date = local_now.date()

    # Determine the last whole day
    if local_now.hour > 0 or local_now.minute > 0 or local_now.second > 0:
        # Yesterday in the specified timezone
        last_whole_day = current_date - timedelta(days=1)
    else:
        # If it's exactly midnight, the last whole day is two days ago
        last_whole_day = current_date - timedelta(days=2)

    # Generate list of n days, ending with the last whole day
    date_list = []
    for i in range(n-1, -1, -1):
        day = last_whole_day - timedelta(days=i)
        date_list.append(day.strftime('%Y-%m-%d'))

    return date_list


def numpy_to_json(arr, devices_list):
    """
    Convert numpy array to JSON-serializable format

    Args:
        arr (numpy.ndarray): 2D numpy array to serialize

    Returns:
        str: JSON string containing array data and metadata
    """
    if not isinstance(arr, np.ndarray):
        raise TypeError("Input must be a numpy array")

    array_dict = {
        'dtype': str(arr.dtype),
        'shape': arr.shape,
        'devices_list': devices_list,
        'data': arr.tolist()  # Convert to nested Python lists
    }

    return json.dumps(array_dict)

def format_time_difference(minutes):
    # Calculate days, hours, minutes
    days = int(minutes // (24 * 60))
    remaining_minutes = minutes % (24 * 60)
    hours = int(remaining_minutes // 60)
    mins = int(remaining_minutes % 60)

    parts = []

    # Add days if any
    if days > 0:
        parts.append(f"{days} day{'s' if days != 1 else ''}")

    # Add hours if any
    if hours > 0:
        parts.append(f"{hours} hour{'s' if hours != 1 else ''}")

    # Add minutes if any
    if mins > 0 or (days == 0 and hours == 0):
        parts.append(f"{mins} minute{'s' if mins != 1 else ''}")

    # Combine the parts into a sentence
    if len(parts) == 1:
        return parts[0]
    elif len(parts) == 2:
        return f"{parts[0]} and {parts[1]}"
    else:
        return f"{parts[0]}, {parts[1]}, and {parts[2]}"

def RunCommand(commmand, args_dictionary, deployment_id):

    to_return = ""
    time_zone_s = GetTimeZoneOfDeployment(deployment_id)
    local_tz = pytz.timezone(time_zone_s)

    filter_minutes = 5
    dates = get_last_n_days(28, time_zone_s)
    ddate = dates[0] #2025-02-02 req.params.get("date")
    to_date = dates[-1]
    date_s = datetime.datetime.now(pytz.UTC).astimezone(local_tz).date().strftime("%Y-%m-%d")

    if commmand == "#STATUS#":
        force_recreate_orig = False #True

        filename = f"/{deployment_id}/{deployment_id}_{ddate}_{to_date}_{filter_minutes}_history_image.png"
        filename = GeneratePresenceHistoryFiles(filename, force_recreate_orig, deployment_id, filter_minutes, ddate, to_date, ddate, time_zone_s)

        date1_obj = datetime.datetime.strptime(ddate, '%Y-%m-%d')
        date2_obj = datetime.datetime.strptime(to_date, '%Y-%m-%d')

        start_date = min(date1_obj, date2_obj)
        end_date = max(date1_obj, date2_obj)
        stretch_by = 30

        # Generate list of all dates
        maps_dates = [
            (start_date + timedelta(days=x)).strftime('%Y-%m-%d')
            for x in range((end_date - start_date).days + 1)
        ]

        day_counter = 0
        minutes_spent_there_list = []
        minutes_locations_list = []
        filename_4w = f"/{deployment_id}/{deployment_id}_{maps_dates[0]}_{maps_dates[-1]}_{filter_minutes}_{stretch_by}_4w_locations.png.bin"
        for ddate in maps_dates:

            timee = LocalDateToUTCEpoch(ddate, time_zone_s)+5 #add so date boundary is avoided
            devices_list, device_ids =  GetProximityList(deployment_id, timee)

            Id2Location = {}
            for device in devices_list:
                Id2Location[device[1]] = device[2]
            Id2Location[0] = "Outside/?"

            filename_day = f"/{deployment_id}/{deployment_id}_{ddate}_{filter_minutes}_{stretch_by}_daily_locations.png.bin"
            locations_list_s = ReadObjectMinIO("daily-maps", filename_day)
            locations_list = ast.literal_eval(locations_list_s)
            minutes_locations_list.append((ddate, locations_list))
            #print(locations_list_s)
            minutes_spent_there = {}


            for loc in Id2Location:
                minutes_spent_there[Id2Location[loc]] = 0
            minutes_spent_there[Id2Location[0]] = 0

            for loc in locations_list:
                minutes_spent_there[Id2Location[loc[0]]] += loc[2]

            for loc in minutes_spent_there:
                minutes_spent_there[loc] = int(1000 * minutes_spent_there[loc] / 1440) / 10
            minutes_spent_there_list.append((ddate, minutes_spent_there))
        data_part = str(minutes_spent_there_list)
        minutes_locations_list_str =  str(minutes_locations_list)
        obj_to_save = {"Location_indexes": str(Id2Location), "Locations": minutes_locations_list_str}
        print(obj_to_save)
        SaveObjectInBlob(filename_4w, obj_to_save)
        print(data_part)

        prompt = "Attached is 4 weeks of data representing % of time where person living alone is spending each day"
        prompt =  prompt +  " Assess  his last week compared to previous 3 weeks. Comment only on significant changes."
        prompt =  prompt +  " Ignore days where data is all (or mostly) 0!"
        prompt =  prompt +  " Consider that office and living room are equivalent for this individual. Entertainment is consumed on computer (office) and in living room TV."
        prompt =  prompt +  " But he is also napping in living room. Comment on his sleeping pattern as well"
        prompt =  prompt +  " Can you summarize all in 1 sentence?"
        prompt =  prompt + " " + data_part
        result = AskGPTPure(prompt)

        to_return = result
        #to_return = "Your father appears to be fine. He was walking around the house 10 minutes ago and is currently in the living room. And I can smell coffee"

    elif commmand == "#STATUS_F#":
        to_return = "Your mother is doing well. She slept 8hr and 23min last night. She used the restroom twice last night. She is now in the kitchen. I can smell coffee."
    elif commmand == "#HELP#":
        to_return  = "There is number of things you can ask me about. For example: 'how is my dad doing?' Or 'How is his environment' or any other question you like"
    elif commmand == "#SLEEP#":
        to_return = "Your dad slept approximately 8 hours last night, took a shower before bed, and got up 4 times during the night."
    elif commmand == "#SLEEP_F#":
        to_return = "Your mom slept approximately 8 hours last night, took a shower before bed, and got up 4 times during the night."
    elif commmand == "#ENVIRONMENT#":
        to_return = "The temperature in the house is 23 degrees Celsius, CO2 level is 662 ppm, and I can smell coffee brewing. Your dad slept approximately 8 hours last night, took a shower before bed, and got up 4 times during the night."
    elif commmand == "#WEEK#":
        to_return = "Showing his weekly activity"
    elif commmand == "#WEEK_F#":
        to_return = "Showing her weekly activity"
    elif commmand == "#ACTIVITY#":
        to_return = "Your dad has been less active this week than usual. He spent more time sitting in the living room and he got up later than usual by 38min. He also did not go outside as frequently and had less visitors. He only showered once this week."
    elif commmand == "#ACTIVITY_F#":
        to_return = "Your mom has been less active this week than usual. She spent more time sitting in the living room and she got up later than usual by 38min. She also did not go outside as frequently and had less visitors. She only showered once this week."
    elif commmand == "#ACTIVITY_COMPARE#":
        to_return = "Overall your dad is less active this year compared to last year. He slept longer in the mornings and had less visitors. Also his shower activity is reduced from typically 2 times a week to once a week."
    elif commmand == "#ACTIVITY_COMPARE_F#":
        to_return = "Overall your mom is less active this year compared to last year. She slept longer in the mornings and had less visitors. Also her shower activity is reduced from typically 2 times a week to once a week."
    elif commmand == "#LOCATION#":
        filterr = 5
        details = GetSensorsDetailsFromDeployment(deployment_id, date_s, filterr)
        #current_time = datetime.datetime.now()
        current_time = datetime.datetime.now(datetime.timezone.utc)
        last_location = details["last_location"]
        detected_time = datetime.datetime.fromisoformat(details["last_detected_time"])
        local_time = local_tz.localize(detected_time)
        # Convert to UTC
        detected_utc_time = local_time.astimezone(pytz.UTC)

        time_diff = current_time - detected_utc_time
        minutes = time_diff.total_seconds() / 60
        time_sentence = format_time_difference(minutes)
        if  minutes == 0:
            to_return = f"He is now in the {last_location}."
        else:
            to_return = f"He was last detected in the {last_location} {time_sentence} ago"
    elif commmand == "#SHOWER#":
        to_return = "In the last 7 days, your Dad took a shower on Friday, Sunday and Tuesday"
    elif commmand == "#SHOWER_F#":
        to_return = "The last time your mom took a shower was Yesterda at 9:33AM"
    elif commmand == "#BATHROOM#":
        to_return = "Last night your Dad used the restroom only once at 6.10am"
    elif commmand == "#KITCHEN#":
        to_return = "Your Dad only cooked Dinner on Wednesday and he turned off the stove afterwards"
    elif commmand == "#MOLD#":
        to_return = "I cannot smell any mold. Also, the humidity is very low. In any of the rooms never exceeded 27% RH in the last 7 days."
    elif commmand == "#VISITORS#":
        to_return = "Yes, on Tuesday, I could detect motion in both office and kitchen at the same time and CO2 levels in the living room exceeded 900ppm."
    elif commmand == "#TEMPERATURE#":
        filterr = 5
        details = GetSensorsDetailsFromDeployment(deployment_id, date_s, filterr)
        current_time = datetime.datetime.now(datetime.timezone.utc)
        last_location = details["last_location"]
        temperature = int(details["temperature"])
        if "America" in time_zone_s:
            temperature_sentence = f"{int(CelsiusToFahrenheit(temperature))} degrees Farenhight"
        else:
            temperature_sentence = f"{temperature} degrees Celsius."
        to_return = f"The temperature in the {last_location} is {temperature_sentence}."
    elif commmand == "#TEMPERATURE_B#":
        to_return = "The temperature in the main bathroom is 80 degrees Farenhight."
    elif commmand == "#OXYGEN#":
        to_return = "His last oxygen level was at 95%."
    elif commmand == "#OXYGEN_F#":
        to_return = "Her last oxygen level was at 95%."
    elif commmand == "#HEART_RATE#":
        to_return = "His last heart rate was 74 bpm."
    elif commmand == "#BLOOD_PRESSURE#":
        to_return = "His latest blood pressure was measured 5 hours ago and it was 137 over 83."
    elif commmand == "#BLOOD_PRESSURE_F#":
        to_return = "Her latest blood pressure was measured 5 hours ago and it was 137 over 83."
    elif commmand == "#EKG#":
        to_return = "His latest HeartBeam EKG was done on Monday and it was within his baseline!"
    elif commmand == "#EKG_F#":
        to_return = "Her latest HeartBeam EKG was done on Monday and it was within her baseline!"
    return to_return

def ScaleToCommon(data, sensor):

    if sensor == "temperature":
        new_min = 0
        new_max = 100
    elif sensor == "humidity":
        new_min = 100
        new_max = 200
    elif sensor == "light":
        new_min = 200
        new_max = 300
    elif sensor == "radar":
        new_min = 300
        new_max = 400
    elif sensor == "s0":
        new_min = 400
        new_max = 500
    elif sensor == "s1":
        new_min = 500
        new_max = 600
    elif sensor == "s2":
        new_min = 600
        new_max = 700
    elif sensor == "s3":
        new_min = 700
        new_max = 800
    elif sensor == "s4":
        new_min = 800
        new_max = 900
    elif sensor == "s5":
        new_min = 900
        new_max = 1000
    elif sensor == "s6":
        new_min = 1000
        new_max = 1100
    elif sensor == "s7":
        new_min = 1100
        new_max = 1200
    elif sensor == "s8":
        new_min = 1200
        new_max = 1300
    else: #s9
        new_min = 1300
        new_max = 1400

    # Split timestamps and values into separate arrays
    timestamps = np.array([x[0] for x in data])
    values = np.array([x[1] for x in data])

    # Get current min and max
    if len(values) > 0:
        current_min = np.min(values)
        current_max = np.max(values)
    else:
        current_min = 0;
        current_max = 0;

    # Scale the values using the min-max formula
    if current_max - current_min > 0:
        scaled_values = (values - current_min) * (new_max - new_min) / (current_max - current_min) + new_min
    else:
        mid_val = (new_max + new_min) / 2
        scaled_values = np.full_like(values, mid_val)

    # Zip back together with original timestamps
    return list(zip(timestamps, scaled_values))

def CreateLocationsStripe(locations_file, time_zone_s):

    parts = locations_file.split("/")
    parts1 = parts[2].split("_")
    ddate = parts1[1]
    deployment_id = parts1[0]
    filter_minutes = parts1[2]
    bw = False
    chart_type = 4
    force_recreate = True
    motion = False
    scale_global = False
    fast = True
    GenerateFullLocationMap(locations_file, deployment_id, ddate, force_recreate, chart_type, bw, motion, scale_global, fast, time_zone_s, filter_minutes)


def  CelsiusToFahrenheit(C):
    F = (C * 9/5) + 32
    return F

def CelsiusToFahrenheitList(compressed_readings: List[Tuple[datetime.datetime, np.float64]]) -> List[Tuple[datetime.datetime, np.float64]]:

    # Create a new list with converted temperatures
    converted_readings = [
        [reading[0], CelsiusToFahrenheit(reading[1])]
        for reading in compressed_readings
    ]

    return converted_readings

def GetPriviledgesOnly(user_name):
    with get_db_connection() as conn:
        if isinstance(user_name, (int)) or user_name.isdigit():
            sql = "SELECT access_to_deployments FROM public.person_details WHERE user_id = " + user_name
        else:
            sql = "SELECT access_to_deployments FROM public.person_details WHERE user_name = '" + user_name + "'"

        with conn.cursor() as cur:
            cur.execute(sql)
            result = cur.fetchall()#cur.fetchone()
            if  result != None:
                return result[0][0]
            else:
                return "0"

def AddToLog(message):
    """Add message to log"""
    logger.info(message)

def FillFields(blob_data, record, form_type):
    """
    Fill in the input fields in the HTML blob_data with values from the caretaker dictionary.

    :param blob_data: str - The initial HTML string containing empty or placeholder input fields.
    :param caretaker: dict - The dictionary containing values to populate the fields.
    :return: str - The HTML string with the input fields filled with the appropriate values.
    """
    # Ensure blob_data is a string
    #blob_data = str(blob_data)

    # Populate the fields
    for field in record:
        logger.debug(f"field= {field}")
        if field == "user_id":
            if record[field] is not None:
                escaped_string = html.escape(str(record[field]))
                # Create a regex pattern to match the span with specific id
                pattern = rf'(<span[^>]+id="editing_user_id"[^>]*>)([^<]*)(</span>)'
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)}{escaped_string}{m.group(3)}', blob_data)
        elif field == "deployment_id":
            if record[field] is not None:
                escaped_string = html.escape(str(record[field]))
                # Create a regex pattern to match the span with specific id
                pattern = rf'(<span[^>]+id="editing_deployment_id"[^>]*>)([^<]*)(</span>)'
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)}{escaped_string}{m.group(3)}', blob_data)
        elif field == "device_id":
            if record[field] is not None:
                escaped_string = html.escape(str(record[field]))
                # Create a regex pattern to match the span with specific id
                pattern = rf'(<span[^>]+id="editing_device_id"[^>]*>)([^<]*)(</span>)'
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)}{escaped_string}{m.group(3)}', blob_data)
        elif field == "user_name":
            if record[field] != None:
                escaped_string = html.escape(record[field])
                pattern = rf'(<input[^>]+id="new_user_name"[^>]+value=")[^"]*(")'
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)}{escaped_string}{m.group(2)}', blob_data)

                # Add value attribute if it does not exist
                pattern = rf'(<input[^>]+id="new_user_name"[^>]*)(>)'
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)} value="{escaped_string}"{m.group(2)}', blob_data)

        elif field == "location":
            if record[field] != None:
                blob_data = SelectOption(blob_data, 'location', record[field])

        elif field == "gender":
            if record[field] != None:
                blob_data = SelectOption(blob_data, 'gender', record[field])

        elif field == "race":
            if record[field] != None:
                blob_data = SelectOption(blob_data, 'race', record[field])

        elif field == "time_zone_s":
            if record[field] != None:
                blob_data = SelectOption(blob_data, 'time_zone_s', record[field])

        elif field == "time_edit" or field == "user_edit":
            pass
        else:
            if record[field] != None:
                escaped_string = html.escape(str(record[field]))
                pattern = rf'(<input[^>]+id="{field}"[^>]+value=")[^"]*(")'
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)}{escaped_string}{m.group(2)}', blob_data)

                # Add value attribute if it does not exist
                pattern = rf'(<input[^>]+id="{field}"[^>]*)(>)'
                blob_data = re.sub(pattern, lambda m: f'{m.group(1)} value="{escaped_string}"{m.group(2)}', blob_data)

    return blob_data

def convert_timestamps_lc(data, time_zone_s):
    target_tz = pytz.timezone(time_zone_s)
    return [[datetime.datetime.fromtimestamp(epoch, pytz.UTC).astimezone(target_tz), value]
            for epoch, value in data]


subbedToL = [("/wellget",1),("/wellget_cmp",1),("/well_hub",1)]
def on_connectL(client_, userdata, flags, rc):
    print(MQTTSERVERL + " L. Connected with result code "+str(rc))

    # Subscribing in on_connect() means that if we lose the connection and
    # reconnect then subscriptions will be renewed.
    client_.subscribe(subbedToL)
    print("SubscribedL to: "+str(subbedToL))

def on_messageL(client_, userdata, msg): #message from GUI
    print(msg.topic+" "+str(msg.payload))
    #msga = msg.payload.decode("ascii")
    #print(msg.timestamp)
    #in_queue.append((str(time.time()), msg.topic, msg.payload))

def MQSendL(topic, content, qos=1):
    print(topic, content[0:100])
    #return MQSend(topic, content)
    #currentTime = int(time.time())
    try:
        if "_cmp" in topic:
            enc_msg = zlib.compress(content.encode('utf-8'))
        else:
            enc_msg = content
        clientL.publish(topic, enc_msg, qos=qos, retain=False)
    except Exception as err:
        print ("Err2B:", err)
        try:
            clientL.disconnect()
            #client.username_pw_set('telegraf', 'well18')
            clientL.connect(MQTTSERVERL, MQTT_PortL, 60)
        except Exception as e:
            print ("Err3b:", e)

# CORS Middleware
class CORSMiddleware:
    def process_request(self, req, resp):
        resp.set_header('Access-Control-Allow-Origin', '*')
        resp.set_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
        resp.set_header('Access-Control-Allow-Headers', '*')

    def process_response(self, req, resp, resource, req_succeeded):
        if req.method == 'OPTIONS':  # Handle preflight requests
            resp.status = falcon.HTTP_200

# Add this class to your code
class RequestParser:
    def __init__(self):
        # Detect if we're running in debug/development mode
        self.debug_mode = __name__ == "__main__" or os.environ.get('DEBUG', 'false').lower() in ('true', '1', 'yes')
        logger.debug(f"RequestParser initialized in {'DEBUG' if self.debug_mode else 'PRODUCTION'} mode")

    def process_request(self, req, resp):
        """Pre-process the request to ensure media is parsed early"""
        logger.debug(f"RequestParser processing: {req.method} {req.path}")

        # Initialize an empty form_data dict
        req.context.form_data = {}

        # Only process POST requests with the right content type
        if req.method != 'POST' or not req.content_type or 'form-urlencoded' not in req.content_type:
            logger.debug("RequestParser: Skipping (not a form POST)")
            return

        try:
            # Different handling based on environment
            if self.debug_mode:
                self._process_debug(req)
            else:
                self._process_production(req)

        except Exception as e:
            logger.error(f"RequestParser error: {str(e)}")
            logger.error(traceback.format_exc())

    def _process_debug(self, req):
        """Process request in debug mode - optimized for local development"""
        logger.debug("RequestParser: Using DEBUG mode processing")

        # In debug mode, we can use Content-Length and know it's reliable
        content_length = req.get_header('content-length')

        if content_length:
            # Content-Length is present
            content_length = int(content_length)
            logger.debug(f"RequestParser: Reading {content_length} bytes using Content-Length")

            raw_body = req.stream.read(content_length)
            if raw_body:
                body_text = raw_body.decode('utf-8')
                logger.debug(f"RequestParser: Successfully read {len(body_text)} chars")

                # Parse the form data
                import urllib.parse
                form_data = dict(urllib.parse.parse_qsl(body_text))

                # Store in context
                req.context.form_data = form_data
                logger.debug(f"RequestParser: Parsed form data: {form_data}")

                # Reset the stream with the original content
                import io
                req.stream = io.BytesIO(raw_body)
            else:
                logger.debug("RequestParser: No body data read")
        else:
            logger.debug("RequestParser (debug): No Content-Length header")

    def _process_production(self, req):
        """Process request in production mode - optimized for OpenFaaS/faasd deployment"""
        logger.debug("RequestParser: Using PRODUCTION mode processing")

        # Simple direct read approach for production (OpenFaaS/faasd)
        # We'll limit the read to 1MB for safety
        MAX_SIZE = 1024 * 1024  # 1MB

        # Just read directly from the stream without checking
        raw_body = req.stream.read(MAX_SIZE)
        if raw_body:
            body_text = raw_body.decode('utf-8')
            logger.debug(f"RequestParser: Successfully read {len(body_text)} chars")

            # Parse the form data
            import urllib.parse
            form_data = dict(urllib.parse.parse_qsl(body_text))

            # Store in context
            req.context.form_data = form_data
            logger.debug(f"RequestParser: Parsed form data: {form_data}")

            # Reset the stream with the original content
            import io
            req.stream = io.BytesIO(raw_body)
        else:
            logger.debug("RequestParser: No body data read")




# Add this function to your code
def get_form_data(req):
    """Helper function to get form data from either context or req.media"""
    # First check if we pre-parsed the form data
    if hasattr(req.context, 'form_data') and req.context.form_data:
        logger.debug("Using pre-parsed form data from context")
        return req.context.form_data

    # Otherwise try to get from req.media (for json)
    try:
        if req.content_type and (
            falcon.MEDIA_JSON in req.content_type or
            falcon.MEDIA_URLENCODED in req.content_type
        ):
            logger.debug("Attempting to get form data from req.media")
            return req.media or {}
    except Exception as e:
        logger.error(f"Error getting req.media: {str(e)}")

    logger.debug("No form data available, returning empty dict")
    return {}

# Path handling middleware
class StripPathMiddleware:
    def process_request(self, req, resp):
        # Strip the '/function/well-api' prefix if present
        path = req.path
        logger.info(f"Original request path: {path}")

        # Define patterns to match different URL formats
        patterns = [
            r'^/function/well-api',  # Standard OpenFaaS path
            r'^/api/well_api',       # API path
        ]

        for pattern in patterns:
            if re.match(pattern, path):
                # Strip the matched prefix
                path = re.sub(pattern, '', path)
                # Ensure path starts with a slash
                if not path.startswith('/'):
                    path = '/' + path
                # Update the request path
                req.path = path
                logger.info(f"Modified request path: {path}")
                break

# --- Database Function Placeholders (Replace with your real implementations) ---
# These should ideally live outside the request handler method
def is_opted_in(phone_number):
    """Checks if a number is already opted in."""
    logger.info(f"[Webhook2 DB] Checking opt-in status for: {phone_number}")
    # Replace with your actual DB logic using the app's DB connection
    print(f"DATABASE STUB: Checking if {phone_number} is opted in.")
    return False # Example stub

def add_opted_in_number(phone_number):
    """Adds the phone number to your opt-in list/database."""
    logger.info(f"[Webhook2 DB] Adding phone number to opt-in list: {phone_number}")
    # Replace with your actual DB logic using the app's DB connection
    print(f"DATABASE STUB: Adding {phone_number} to opt-ins.")
    return True # Example stub
# ---

# Main API class
class WellApi:
    def on_get_healthz(self, req, resp):
        """Health check endpoint"""
        resp.status = HTTP_200
        resp.content_type = falcon.MEDIA_TEXT
        resp.text = "OK"

    def on_get(self, req, resp, path=""):
        """Handle GET requests"""
        logger.debug(f"GET request to path: {path}")
        logger.debug(f"Sent variables: {req.params}")
        logger.debug(f"All headers: {dict(req.headers)}")
        if path == "" or path == "/":
            # Serve the main portal page
            blob_data = read_file("well_portal.html")
            if blob_data:
                resp.content_type = "text/html"
                resp.text = blob_data
            else:
                # Fall back to JSON response if file not found
                resp.media = {"message": "Hello from OpenFaaS Serverless Web Server!", "method": "GET"}
            return
        elif path == "favicon.ico":
            favicon_path = "favicon.ico"
            if os.path.isfile(favicon_path):
                resp.content_type = 'image/x-icon'
                resp.data = read_file(favicon_path, type_="BIN")
                resp.status = HTTP_200
            else:
                resp.status = HTTP_404
            return
        elif path == "health":
            resp.status = HTTP_200
            resp.content_type = falcon.MEDIA_JSON
            resp.text = json.dumps({"status": "healthy"})
            return

        # Authentication and authorization
        token = req.params.get('token')
        user_name = req.params.get('user_name')
        ps = req.params.get('ps')

        if ps != "" and ps != None:
            #was token sent in ps field? This allows for token and ps be populated by token or ps
            user_info = verify_token(ps)
            if user_info["username"] == user_name:
                token = ps
            else:
                #is this valid password?
                privileges, user_id = ValidUser(user_name, ps)
                if privileges == "0":
                    resp.media = package_response("Log-Out", HTTP_401)
                    return
                else:
                    token = generate_token(user_name)



        user_info = verify_token(token)

        if user_info == None or user_info["username"] != user_name:
            resp.media = package_response("Log-Out", HTTP_401)
            return

        get_function_name = req.params.get('name')
        logger.debug(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] - {__name__}.GET_API->{get_function_name}")
        privileges = GetPriviledgesOnly(user_name)

        if (token and user_name) or (token and user_name):
            user_info = verify_token(token)
            if user_info is None or user_info["username"] != user_name:
                resp.media = package_response("Log-Out", HTTP_401)
                return

            get_function_name = req.params.get('name')
            logger.debug(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] - {__name__}.GET_API->{get_function_name}")

            if get_function_name == "deployment_add":
                user_id = req.params.get('user_id')
                blob_data = read_file("edit_deployment.html")
                caretaker = {'deployment_id': 0, 'beneficiary_id': user_id, 'caretaker_id': user_id, 'owner_id': user_id, 'installer_id': user_id, 'user_id': 0, 'role_ids': '2', 'access_to_deployments': '', 'email': '', 'user_name': '', 'first_name': '', 'last_name': '', 'address_street': '', 'address_city': '', 'address_zip': '', 'address_state': '', 'address_country': '', 'phone_number': '', 'picture': '/', 'key': ''}
                blob_data = FillFields(blob_data, caretaker, 1)
                resp.content_type = "text/html"
                resp.text = blob_data
                return

            elif get_function_name == "devices_list":
                st = time.time()
                user_name = req.params.get('user_name')
                privileges = GetPriviledgesOnly(user_name)
                first_s = req.params.get('first')
                last_s = req.params.get('last')

                try:
                    first = int(first_s)
                except ValueError:
                    first = 0

                try:
                    last = int(last_s)
                except ValueError:
                    last = 1000000
                blob_data = read_file("my_devices.html")

                devices = GetVisibleDevices(privileges)
                users = GetUsersFromDeployments(privileges)
                blob_data = UpdateDevicesTable(blob_data, devices, users)
                blob_data = UpdateDeploymentsSelector(blob_data, users)
                resp.content_type = "text/html"
                resp.text = blob_data
                #print(blob_data)
                return

            elif get_function_name == "deployment_edit":
                deployment_id = req.params.get('deployment_id')
                blob_data = read_file("edit_deployment.html")

                deployment = DeploymentDetails(deployment_id)
                #blob_data = blob_data.decode("utf-8")
                blob_data = FillFields(blob_data, deployment, 1)
                resp.content_type = "text/html"
                resp.text = blob_data
                return

            elif get_function_name == "caretaker_add":

                blob_data = read_file("edit_caretaker.html")
                caretaker = {'user_id': 0, 'role_ids': '2', 'access_to_deployments': '', 'email': '', 'user_name': '', 'first_name': '', 'last_name': '', 'address_street': '', 'address_city': '', 'address_zip': '', 'address_state': '', 'address_country': '', 'phone_number': '', 'picture': '/', 'key': ''}
                blob_data = FillFields(blob_data, caretaker, 1)
                resp.content_type = "text/html"
                resp.text = blob_data
                return
            elif get_function_name == "caretaker_edit":
                user_id = req.params.get('user_id')
                blob_data = read_file("edit_caretaker.html")

                caretaker = UserDetails(user_id)
                #blob_data = blob_data.decode("utf-8")
                blob_data = FillFields(blob_data, caretaker, 1)
                resp.content_type = "text/html"
                resp.text = blob_data
                return

            elif get_function_name == "device_add":

                blob_data = read_file("edit_device.html")
                device = {'device_id': 0, 'device_mac': '', 'well_id': '', 'description': '', 'location': '', 'close_to': '', 'radar_threshold': '["s3_max",12]', 'temperature_calib': '0.0,1.0,0.0', 'humidity_calib': '0.0,1.0,0.0'}
                blob_data = FillFields(blob_data, device, 1)
                resp.content_type = "text/html"
                resp.text = blob_data
                return
            elif get_function_name == "device_edit":
                mac = req.params.get('mac')

                blob_data = read_file("edit_device.html")

                device_det = DeviceDetails(mac)
                if device_det['radar_threshold'] == None or device_det['radar_threshold'] == "":
                    device_det['radar_threshold'] = '["s3_max",12]'
                #blob_data = blob_data.decode("utf-8")
                blob_data = FillFields(blob_data, device_det, 1)
                resp.content_type = "text/html"
                resp.text = blob_data
                return


            elif get_function_name == "beneficiary_edit":
                user_id = req.params.get('user_id')
                blob_data = read_file("edit_beneficiary.html")

                beneficiary = UserDetails(user_id)
                #blob_data = blob_data.decode("utf-8")
                blob_data = FillFields(blob_data, beneficiary, 1)
                resp.content_type = "text/html"
                resp.text = blob_data
                return
            elif get_function_name == "beneficiary_add":

                blob_data = read_file("edit_beneficiary.html")
                beneficiary = {'user_id': 0, 'role_ids': '1', 'access_to_deployments': '', 'email': '', 'user_name': '', 'first_name': '', 'last_name': '', 'address_street': '', 'address_city': '', 'address_zip': '', 'address_state': '', 'address_country': '', 'phone_number': '', 'picture': '/', 'key': ''}
                blob_data = FillFields(blob_data, beneficiary, 1)
                resp.content_type = "text/html"
                resp.text = blob_data
                return
            elif get_function_name == "get_image_file":
                #image represents day in local time

                deployment_id = req.params.get('deployment_id')
                time_zone_s = GetTimeZoneOfDeployment(deployment_id)
                ddate = req.params.get("date")
                ddate = ddate.replace("_","-")
                group_by = req.params.get("group_by")
                timee = StringToEpoch(ddate, time_zone_s)
                force_recreate = req.params.get("re_create") == "true"
                radar_part = req.params.get("radar_part")
                map_type = int(req.params.get("map_type"))

                bw = req.params.get("bw") == "true"
                unique_identifier = req.params.get("unique_identifier")
                filename = f"/{deployment_id}/{deployment_id}_{ddate}_{group_by}_{radar_part}_{map_type}_{bw}_dayly_image.png"

                #print(check_file_exists(filename))
                if not force_recreate:
                    file_exists, time_modified_utc = check_file_exists(filename)
                    if file_exists:
                        time_modified_local = time_modified_utc.astimezone(pytz.timezone(time_zone_s))
                        time_modified_date = time_modified_local.date()
                        file_date = MapFileToDate(filename)
                        if time_modified_date <= file_date:
                            force_recreate = True
                    else:
                        force_recreate = True

                #ddate is in Local Time
                timee = LocalDateToUTCEpoch(ddate, time_zone_s)+5 #add so date boundary is avoided
                #time that describes new devices in deployment_history is in UTC therefore timee is in UTC

                st = time.time()
                vocs_scaled = {}
                #file_date is in Local time, so we are comparing that and current Local (to install) Date
                if force_recreate:
                    st = time.time()
                    vocs_scaled = {}
                    devices_list =  GetProximityList(deployment_id, timee)
                    stored, vocs_scaled = CreateMapFast(filename, devices_list, ddate, bw, time_zone_s, radar_part, group_by) #"[bit] 1=same sensors together, 2=same device together, 4=1 der, 8=2 der
                    if stored != True:
                        AddToLog("Map not created")
                        #logger.warning("Map not created")
                        resp.media = package_response("Map not created", HTTP_401)
                        return
                    else:
                        AddToLog("Map created")
                        #lets send over MQTT vocs_scaled
                        json_data = numpy_to_json(vocs_scaled, devices_list)
                        MQSendL("/"+unique_identifier, json_data)
                        #print(time.time() - st)

                #lets read and send image from blob
                image_bytes, content_type = GetBlob(filename)
                if debug:
                    resp.media = package_response(f'Log: {debug_string}', HTTP_200)
                else:
                    if image_bytes is None:
                        raise falcon.HTTPNotFound(
                            title='Image not found',
                            description=f'Image {filename} could not be found or retrieved'
                        )
                    sys.stdout.flush()
                    # Set response content type and body
                    resp.content_type = content_type
                    resp.data = image_bytes
                    resp.status = falcon.HTTP_200
                    return

            elif get_function_name == "get_full_location_map":

                raw = req.params.get("raw") == "true"

                if raw:
                    #function=request_deployment_map_new
                    #token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InJvYnN0ZXIiLCJleHAiOjE3MzgxNzYzNTZ9.5wzC2dVQhKlMygHPZfombTINbltNq8vxdilLIugNTtA&
                    #user_name=robster&
                    #date=2025-01-27&
                    #deployment_id=21&
                    #map_type=2
                    chart_type = 8
                else:
                    chart_type = int(req.params.get("map_type"))

                #image represents day in local time
                logger.debug("get_full_location_map")
                deployment_id = req.params.get('deployment_id')
                time_zone_s = GetTimeZoneOfDeployment(deployment_id)
                ddate = req.params.get("date")
                ddate = ddate.replace("_","-")

                to_date = ddate

                try:
                    to_date = req.params.get("to_date")
                    to_date = to_date.replace("_","-")
                except:
                    pass

                if to_date != ddate:
                    chart_type = int(req.params.get("map_type"))

                force_recreate = req.params.get("re_create") == "true"
                force_recreate_orig = force_recreate
                scale_global = req.params.get("scale_global") == "true"
                fast = req.params.get("fast") == "true"
                bw = req.params.get("bw") == "true"
                motion = req.params.get("motion") == "true"
                timee = StringToEpoch(ddate, time_zone_s)

                filter_minutes = int(req.params.get("filter"))

                if "flavor" in req.params: #this is to be used only when creating
                    flavor = int(req.params.get("flavor"))
                else:
                    flavor = 0

                if bw:
                    bw_s = "BW"
                else:
                    bw_s = "CLR"

                if fast:
                    fast_s = "FAST"
                else:
                    fast_s = "SLOW"

                if motion:
                    motion_s = "M"
                else:
                    motion_s = "S"

                if scale_global:
                    scl_s = "scl"
                else:
                    scl_s = "nscl"


                if chart_type == 5 or chart_type == 7:
                    #now_date = req.params.get("now_date")
                    #now_date = now_date.replace("_","-")
                    filename = f"/{deployment_id}/{deployment_id}_{ddate}_{to_date}_{filter_minutes}_history_image.png"
                else:
                    filename = f"/{deployment_id}/{deployment_id}_{ddate}_{bw_s}_{motion_s}_{scl_s}_{chart_type}_Flocation_image.png"

                if not force_recreate:
                    file_exists, time_modified_utc = check_file_exists(filename)
                    #file_exists1, time_modified_utc1 = check_file_exists(filename+".bin")
                    if file_exists:
                        time_modified_local = time_modified_utc.astimezone(pytz.timezone(time_zone_s))
                        time_modified_date = time_modified_local.date()
                        file_date = MapFileToDate(filename)
                        if time_modified_date <= file_date:
                            force_recreate = True
                        else: #same date
                            current_time = datetime.datetime.now(pytz.timezone(time_zone_s))
                            time_passed = current_time - time_modified_local
                            #if time_passed.seconds > 300: #recreate if older than 5 minutes
                            #    force_recreate = True

                    else:
                        force_recreate = True

                if force_recreate:
                    ddate = ddate.replace("_","-")
                    #filter_minutes = 5
                    #filename = os.path.join(scriptDir+"/daily_maps/"+deployment, proximity_string+"_"+deployment+"_"+ddate+"_dayly_image.png")

                    filename = filename.replace('\\','/')
                    if chart_type == 4: #"collapsed":
                        GenerateFullLocationMap(filename, deployment_id, ddate, force_recreate, chart_type, bw, motion, scale_global, fast, time_zone_s, filter_minutes)
                    elif chart_type == 5: #"history":
                        GeneratePresenceHistory(filename, force_recreate, deployment_id, filter_minutes, ddate, to_date, ddate, time_zone_s)
                    elif chart_type == 7: #"history full chart":
                        filename = GeneratePresenceHistoryChart(filename, force_recreate_orig, deployment_id, filter_minutes, ddate, to_date, ddate, time_zone_s)
                    elif chart_type == 8: #"set for mobile"
                        GenerateFullLocationMapLabelsOut(filename, deployment_id, ddate, force_recreate, chart_type, bw, motion, scale_global, fast,time_zone_s, filter_minutes)
                    else:
                        GenerateFullLocationMap(filename, deployment_id, ddate, force_recreate, chart_type, bw, motion, scale_global, fast,time_zone_s, filter_minutes)

                #lets read and send image from blob
                image_bytes, content_type = GetBlob(filename)

                if image_bytes is None:
                    raise falcon.HTTPNotFound(
                        title='Image not found',
                        description=f'Image {filename} could not be found or retrieved'
                    )

                # Set response content type and body
                resp.content_type = content_type
                resp.data = image_bytes
                resp.status = falcon.HTTP_200
                return

            elif get_function_name == "get_presence_map":
                #image represents day in local time

                deployment_id = req.params.get('deployment_id')
                time_zone_s = GetTimeZoneOfDeployment(deployment_id)
                ddate = req.params.get("date")
                ddate = ddate.replace("_","-")
                force_recreate = req.params.get("re_create") == "true"
                scale_global = req.params.get("scale_global") == "true"
                fast = req.params.get("fast") == "true"
                bw = req.params.get("bw") == "true"
                motion = req.params.get("motion") == "true"
                timee = StringToEpoch(ddate, time_zone_s)
                chart_type = int(req.params.get("map_type"))
                filter_minutes = int(req.params.get("filter"))

                if bw:
                    bw_s = "BW"
                else:
                    bw_s = "CLR"

                if fast:
                    fast_s = "FAST"
                else:
                    fast_s = "SLOW"

                if motion:
                    motion_s = "M"
                else:
                    motion_s = "S"

                if scale_global:
                    scl_s = "scl"
                else:
                    scl_s = "nscl"


                filename = f"/{deployment_id}/{deployment_id}_{ddate}_{bw_s}_{motion_s}_{scl_s}_{chart_type}_Flocation_image.png"

                if not force_recreate:
                    file_exists, time_modified_utc = check_file_exists(filename)
                    if file_exists:
                        time_modified_local = time_modified_utc.astimezone(pytz.timezone(time_zone_s))
                        time_modified_date = time_modified_local.date()
                        file_date = MapFileToDate(filename)
                        if time_modified_date <= file_date:
                            force_recreate = True
                    else:
                        force_recreate = True


                if force_recreate:
                    ddate = ddate.replace("_","-")
                    days = 7

                    filename = filename.replace('\\','/')
                    if chart_type == 6: #"AI Locations":
                        timee = LocalDateToUTCEpoch(ddate, time_zone_s)+5 #add so date boundary is avoided
                        devices_list, device_ids =  GetProximityList(deployment_id, timee)

                        st = time.time()
                        if CreatePresenceMap(filename, devices_list, ddate, 1, force_recreate, chart_type, bw, motion, scale_global, fast, filter_minutes, time_zone_s) == 0: #"[bit] 1=same sensors together, 2=same device together, 4=1 der, 8=2 der
                            print(ddate, "Not found")
                        else:
                            print(ddate, time.time() - st)
                #lets read and send image from blob
                image_bytes, content_type = GetBlob(filename)

                if image_bytes is None:
                    raise falcon.HTTPNotFound(
                        title='Image not found',
                        description=f'Image {filename} could not be found or retrieved'
                    )

                # Set response content type and body
                resp.content_type = content_type
                resp.data = image_bytes
                resp.status = falcon.HTTP_200

                return
            elif get_function_name == "download":

                deployment_id = req.params.get('deployment_id')
                time_zone_s = GetTimeZoneOfDeployment(deployment_id)
                date_from = req.params.get("date_from")
                date_to = req.params.get("date_to")
                date_from = date_from.replace("_","-")
                date_to = date_to.replace("_","-")
                consolidated_by = req.params.get("consolidated_by")
                force_recreate = req.params.get("re_create") == "true"
                radar_part = req.params.get("radar_part")
                zip_filename = f"/{deployment_id}/{deployment_id}_{date_from}_{date_to}_{consolidated_by}_data.zip"

                #print(check_file_exists(filename))
                if not force_recreate:
                    file_exists, time_modified_utc = check_file_exists(zip_filename, bucket_name="data-downloads")
                    if file_exists:
                        time_modified_local = time_modified_utc.astimezone(pytz.timezone(time_zone_s))
                        time_modified_date = time_modified_local.date()
                        file_date = CSVFileToDate(zip_filename)
                        if time_modified_date <= file_date:
                            force_recreate = True
                    else:
                        force_recreate = True

                #ddate is in Local Time
                dates = DatesSpan(date_from, date_to)
                to_zip = []
                for ddate in dates:
                    force_recreate_csv = force_recreate
                    csv_dayly_filename = f"/{deployment_id}/{deployment_id}_{ddate}_{consolidated_by}_data.csv"
                    timee = LocalDateToUTCEpoch(ddate, time_zone_s)+5 #add so date boundary is avoided
                    if not force_recreate_csv:
                        #time that describes new devices in deployment_history is in UTC therefore timee is in UTC
                        file_exists, time_modified_utc = check_file_exists(csv_dayly_filename, bucket_name="data-downloads")
                        if file_exists:
                            time_modified_local = time_modified_utc.astimezone(pytz.timezone(time_zone_s))
                            time_modified_date = time_modified_local.date()
                            file_date = CSVFileToDate(csv_dayly_filename)
                            if time_modified_date <= file_date:
                                force_recreate_csv = True
                        else:
                            force_recreate_csv = True
                    st = time.time()
                    vocs_scaled = {}
                    #file_date is in Local time, so we are comparing that and current Local (to install) Date
                    if force_recreate_csv:
                        st = time.time()
                        vocs_scaled = {}

                        devices_list = GetProximityList(deployment_id, timee)
                        temp_offset = -16
                        file_stored = CreateDailyCSV(csv_dayly_filename, devices_list, ddate, vocs_scaled, time_zone_s, radar_part, consolidated_by, temp_offset) #"[bit] 1=same sensors together, 2=same device together, 4=1 der, 8=2 der
                        to_zip.append(file_stored)
                    else:
                        to_zip.append(csv_dayly_filename)

                if to_zip:
                    success = zip_blobs(
                        blob_paths=to_zip,
                        zip_blob_name=zip_filename,
                        bucket_name="data-downloads",
                        minio_client=miniIO_blob_client
                    )

                    if success:
                        print("Files successfully zipped")
                    else:
                        print("Error occurred while zipping files")

                #pack CSV files from BLOB into ZIP
                #lets read and send image from blob
                zip_bytes, content_type = GetBlob(zip_filename, bucket_name="data-downloads")
                if debug:
                    resp.media = package_response(f'Log: {debug_string}', HTTP_200)
                else:
                    if zip_bytes is None:
                        raise falcon.HTTPNotFound(
                            title='File not found',
                            description=f'File {zip_filename} could not be found or retrieved'
                        )

                    # Set response content type and body
                    resp.content_type = content_type
                    resp.data = zip_bytes
                    resp.status = falcon.HTTP_200
                    return
            resp.media = package_response("Use POST method for this endpoint", HTTP_400)

        # Default response for unmatched paths
        #resp.media = package_response(f"Path: /{path}", HTTP_200)

    def on_post(self, req, resp, path=""):
        """Handle POST requests"""
        logger.debug(f"on_post called with path: {path}")
        logger.debug(f"Request method: {req.method}")
        logger.debug(f"Request path: {req.path}")
        logger.debug(f"Request query string: {req.query_string}")
        logger.debug(f"Request headers: {req.headers}")
        logger.debug(f"Request content type: {req.content_type}")

        # Get form data using our helper function - but don't read stream again
        form_data = get_form_data(req)
        logger.debug(f"Form data: {form_data}")

        ## Special cases for specific endpoints
        if path == "telnyx-sms-webhook":

            #SMS webhook: https://eluxnetworks.net/function/well-api/api/telnyx-sms-webhook
            #Voice call webhook: https://eluxnetworks.net/function/well-api/api/telnyx-call-webhook
            try:
                # Parse the webhook payload
                webhook_data = json.loads(req.stream.read().decode('utf-8'))
                logger.info(f"Received Telnyx SMS webhook: {webhook_data}")

                # Handle the SMS webhook (example for message.received event)
                if webhook_data.get('data', {}).get('event_type') == 'message.received':
                    # Extract important information
                    payload = webhook_data.get('data', {}).get('payload', {})
                    from_number = payload.get('from', {}).get('phone_number')
                    to_number = payload.get('to', [{}])[0].get('phone_number') if payload.get('to') else None
                    message_text = payload.get('text', '')

                    logger.info(f"SMS from {from_number} to {to_number}: {message_text}")

                    # Here you can implement your business logic to handle the SMS
                    # For example, storing in database, triggering actions, etc.

                # Always respond with 200 OK to acknowledge receipt
                resp.status = HTTP_200
                resp.content_type = falcon.MEDIA_JSON
                resp.text = json.dumps({"status": "received"})
                return

            except Exception as e:
                logger.error(f"Error processing Telnyx SMS webhook: {str(e)}")
                resp.status = HTTP_500
                resp.content_type = falcon.MEDIA_JSON
                resp.text = json.dumps({"error": str(e)})
                return
        # --- START: New Webhook Handler for telnyx-sms-webhook2 ---
        elif path == "telnyx-sms-webhook2":
            logger.info("Handling request for telnyx-sms-webhook2")
            try:
                # 1. Read Raw Payload (needed for signature validation and parsing)
                # Ensure the stream hasn't been read previously in the request lifecycle
                raw_payload = req.stream.read()
                if not raw_payload:
                    logger.warning("Request stream was empty for telnyx-sms-webhook2.")
                    resp.status = falcon.HTTP_BAD_REQUEST
                    resp.media = {"error": "Missing request body"}
                    return

                # 2. (Optional but Recommended) Signature Validation
                if TELNYX_PUBLIC_KEY:
                    signature = req.headers.get("TELNYX-SIGNATURE-ED25519") # Case-insensitive lookup
                    timestamp = req.headers.get("TELNYX-TIMESTAMP")
                    if not signature or not timestamp:
                        logger.warning("Missing Telnyx signature headers for telnyx-sms-webhook2")
                        resp.status = falcon.HTTP_BAD_REQUEST # Use 400 Bad Request
                        resp.media = {"error": "Missing signature headers"}
                        return
                    try:
                        from telnyx.webhook import Webhook # Import here or globally
                        # Ensure 'telnyx' library is installed: pip install telnyx
                        Webhook.construct_event(raw_payload, signature, timestamp, TELNYX_PUBLIC_KEY, tolerance=600)
                        logger.info("Telnyx signature validated successfully for telnyx-sms-webhook2.")
                    except Exception as e:
                        logger.error(f"Invalid Telnyx signature for telnyx-sms-webhook2: {e}")
                        resp.status = falcon.HTTP_UNAUTHORIZED # Use 401 Unauthorized
                        resp.media = {"error": "Invalid signature"}
                        return
                else:
                    logger.warning("TELNYX_PUBLIC_KEY not set. Skipping signature validation for telnyx-sms-webhook2.")


                # 3. Decode and Parse JSON
                payload_data = json.loads(raw_payload.decode('utf-8'))
                logger.debug(f"Webhook 2 Payload: {json.dumps(payload_data)}") # Debug log payload

                # 4. Process Logic (similar to standalone webhook)
                event_type = payload_data.get('data', {}).get('event_type')
                record_type = payload_data.get('data', {}).get('record_type')

                if event_type == 'message.received' and record_type == 'message':
                    message_payload = payload_data.get('data', {}).get('payload', {})
                    direction = message_payload.get('direction')

                    if direction == 'inbound':
                        from_number = message_payload.get('from', {}).get('phone_number')
                        message_body = message_payload.get('text', '').strip().upper()

                        logger.info(f"[Webhook2] Inbound message from {from_number}. Checking keyword.")

                        # Check for the specific opt-in keyword
                        if message_body == OPT_IN_KEYWORD:
                            logger.info(f"[Webhook2] Opt-in keyword '{OPT_IN_KEYWORD}' detected from {from_number}")
                            if not is_opted_in(from_number): # Check DB
                                if add_opted_in_number(from_number): # Add to DB
                                    logger.info(f"[Webhook2] Successfully processed opt-in for {from_number}")
                                else:
                                    logger.error(f"[Webhook2] Failed to add {from_number} to opt-in database.")
                                    # Decide if this is a server error
                                    # resp.status = falcon.HTTP_INTERNAL_SERVER_ERROR
                                    # resp.media = {"error": "Database operation failed"}
                                    # return # Optionally return early on DB error
                            else:
                                logger.info(f"[Webhook2] {from_number} is already opted in.")
                        # Add elif for STOP/HELP handling here if needed
                        else:
                            logger.info(f"[Webhook2] Message from {from_number} did not match opt-in keyword.")

                # 5. Acknowledge Receipt to Telnyx (Use 204 No Content)
                resp.status = falcon.HTTP_204
                # No body needed for 204 response
                return # Important: return after handling

            except json.JSONDecodeError:
                logger.error("Failed to decode JSON from request body for telnyx-sms-webhook2")
                resp.status = falcon.HTTP_BAD_REQUEST
                resp.media = {"error": "Invalid JSON payload"}
                return
            except Exception as e:
                logger.exception("Error processing telnyx-sms-webhook2") # Log full traceback
                resp.status = falcon.HTTP_INTERNAL_SERVER_ERROR
                resp.media = {"error": "Internal server error"}
                return
        # --- END: New Webhook Handler for telnyx-sms-webhook2 ---
        elif path == "telnyx-call-webhook":
            try:
                # Parse the webhook payload
                webhook_data = json.loads(req.stream.read().decode('utf-8'))
                logger.info(f"Received Telnyx Call webhook: {webhook_data}")

                # Handle different voice call events
                event_type = webhook_data.get('data', {}).get('event_type')
                payload = webhook_data.get('data', {}).get('payload', {})

                if event_type == 'call.initiated':
                    # Call is being initiated
                    call_control_id = payload.get('call_control_id')
                    from_number = payload.get('from')
                    to_number = payload.get('to')
                    logger.info(f"Call initiated from {from_number} to {to_number}, control ID: {call_control_id}")

                    # Store the call_control_id if you need to control this call later

                elif event_type == 'call.answered':
                    # Call was answered
                    call_control_id = payload.get('call_control_id')
                    call_session_id = payload.get('call_session_id')
                    logger.info(f"Call answered, session ID: {call_session_id}")

                    # If you want to play your MP3 when the call is answered, this is where you'd
                    # make another API call to Telnyx with the call_control_id to start playback

                elif event_type == 'playback.started':
                    # Playback of audio file has started
                    call_control_id = payload.get('call_control_id')
                    logger.info(f"Audio playback started for call: {call_control_id}")

                elif event_type == 'playback.ended':
                    # Playback of audio file has ended
                    call_control_id = payload.get('call_control_id')
                    logger.info(f"Audio playback ended for call: {call_control_id}")

                # Always respond with 200 OK to acknowledge receipt
                resp.status = HTTP_200
                resp.content_type = falcon.MEDIA_JSON
                resp.text = json.dumps({"status": "received"})
                return

            except Exception as e:
                logger.error(f"Error processing Telnyx call webhook: {str(e)}")
                resp.status = HTTP_500
                resp.content_type = falcon.MEDIA_JSON
                resp.text = json.dumps({"error": str(e)})
                return
        #if path == "users":
            #logger.info("POST request to users endpoint")
            #resp.status = HTTP_201
            #resp.content_type = falcon.MEDIA_JSON
            #resp.text = json.dumps({"id": "new-user-id", "message": "User created"})
            #return
        #elif path == "items":
            #logger.info("POST request to items endpoint")
            #resp.status = HTTP_201
            #resp.content_type = falcon.MEDIA_JSON
            #resp.text = json.dumps({"id": "new-item-id", "message": "Item created"})
            #return

        try:
            # Get basic parameters
            function = form_data.get('function')
            user_name = form_data.get('user_name')
            logger.debug(f"Function: {function}, User: {user_name}")


            if function != "credentials":
                token = form_data.get('token')
                ps = form_data.get('ps')


                if ps != "" and ps != None:
                    #was token sent in ps field? This allows for token and ps be populated by token or ps
                    user_info = verify_token(ps)
                    if  user_info != None:
                        if user_info["username"] == user_name:
                            token = ps
                    else:
                        #is this valid password?
                        privileges, user_id = ValidUser(user_name, ps)
                        if privileges == "0":
                            resp.media = package_response("Log-Out", HTTP_401)
                            return
                        else:
                            token = generate_token(user_name)

                user_info = verify_token(token)

                if user_info == None or user_info["username"] != user_name:
                    resp.media = package_response("Log-Out", HTTP_401)
                    return


                #with get_db_connection() as db_conn:
                privileges = GetPriviledgesOnly(user_name)

            # Handle credentials function - most common case
            if function == "credentials":

                clientId = form_data.get('clientId')
                nonce = form_data.get('nonce')
                ps = form_data.get('ps')

                if not user_name:
                    resp.media = package_response("Required field 'user_name' is missing", HTTP_400)
                    return

                if not clientId:
                    resp.media = package_response("Required field 'clientId' is missing", HTTP_400)
                    return

                if not nonce:
                    resp.media = package_response("Required field 'nonce' is missing", HTTP_400)
                    return

                if not ps:
                    resp.media = package_response("Required field 'ps' is missing", HTTP_400)
                    return



                if False:
                    pass
                else:
                    #lets check for real
                    privileges, user_id = ValidUser(user_name, ps)
                    if privileges == "0":
                        access_token = 0
                        privileges = 0
                    else:
                        access_token = generate_token(user_name)

                token_payload = {'access_token': access_token, 'privileges': privileges, 'user_id': user_id}
                resp.media = package_response(token_payload)
                resp.status = falcon.HTTP_200
                return

            # Handle token-protected functions
            elif function == "messages_age":

                macs = form_data.get('macs')

                with get_db_connection() as conn:

                    #print (sqlr)
                    with conn.cursor() as cur:

                        devices = MACsStrToDevIds(cur, macs)

                        devices_string = ",".join(f"{device_id}" for mac, device_id in devices)


                        sqlr = f"""
                        SELECT
                            device_id,
                            GREATEST(
                              radar_last_time,
                            sensor_last_time
                            ) AS latest_time
                        FROM
                            (SELECT unnest(ARRAY[{devices_string}]) AS device_id) d
                        LEFT JOIN LATERAL (
                            SELECT time AS radar_last_time
                          FROM radar_readings
                          WHERE device_id = d.device_id
                          ORDER BY time DESC
                          LIMIT 1
                        ) r ON true
                        LEFT JOIN LATERAL (
                            SELECT time AS sensor_last_time
                          FROM sensor_readings
                          WHERE device_id = d.device_id
                          ORDER BY time DESC
                          LIMIT 1
                        ) s ON true;"""
                        logger.debug(f"sqlr= {sqlr}")
                        cur.execute(sqlr)
                        times_list = cur.fetchall()
                        result = {}
                        for i in range(len(times_list)):
                            if times_list[i][1] is not None:
                                result[devices[i][0]] = times_list[i][1].timestamp()
                            else:
                                result[devices[i][0]] = 0

                dataa = {}
                dataa['Command'] = "REPORT"
                dataa['body'] = result
                dataa['time'] = time.time()
                #json_data = json.dumps(dataa)
                payload = {'ok': True, 'response': dataa}
                resp.media = package_response(payload)
                logger.warning(f"Responded: {str(payload)}")
                resp.status = falcon.HTTP_200
                return

            elif function == "voice_ask":

                question = form_data.get('question')
                deployment_id = form_data.get('deployment_id')

                if ('language_from' in form_data):
                    language_from = form_data.get('language_from').strip()
                else:
                    language_from = "English"

                if ('language_to' in form_data):
                    language_to = form_data.get('language_to').strip()
                else:
                    language_to = "English"


                result, language = AskGPT(question, language_from, language_to)

                if result[0] == "#":
                    result = RunCommand(result, {}, deployment_id)

                dataa = {}
                dataa['Command'] = "REPORT"
                dataa['body'] = result
                dataa['name'] = ""
                dataa['reflected'] = ""
                dataa['language'] = language
                dataa['time'] = time.time()
                #json_data = json.dumps(dataa)
                payload = {'ok': True, 'response': dataa}
                resp.media = package_response(payload)
                logger.warning(f"Responded: {str(payload)}")
                resp.status = falcon.HTTP_200
                return


            elif function == "calibrate_thresholds":
                #this will use current date to calibrate radar presence thresholds.
                #make sure that data is well defined (has clear absence/presence signature) for all rooms for chosen day
                #Format of radar_threshold field = [gates_to_use_Presence_list, p_threshold]
                #We need to automate this functionality!!!
                deployment_id = form_data.get('deployment_id')
                time_zone_s = GetTimeZoneOfDeployment(deployment_id)
                ddate = form_data.get("date")
                ddate = ddate.replace("_","-")
                selected_date = ddate



                stdev_range = int(form_data.get("stdev_range"))
                timee = LocalDateToUTCEpoch(ddate, time_zone_s)+5
                devices_list, device_ids =  GetProximityList(deployment_id, timee)

                selected_date = FindCalibrationDate(device_ids, ddate)

                devices_c = len(devices_list[0])

                time_from_str, time_to_str = GetLocalTimeForDate(selected_date, time_zone_s, stdev_range)
                fields = ["radar_s_min", "radar_s_max", "radar_m_max", "radar_stdev"]
                cnt = 0
                ids_list = []
                for details in devices_list:
                    ids_list.append(details[1])
                devices_list_str = ",".join(map(str, ids_list))
                device_to_index = {device: idx for idx, device in enumerate(ids_list)}

                minutes = 1440

                with get_db_connection() as conn:
                    with conn.cursor() as cur:
                        for device_index in range(devices_c):
                            well_id = devices_list[device_index][0]
                            device_id = devices_list[device_index][1]
                            location = devices_list[device_index][2]

                            sql = get_device_radar_s28_only_query(time_from_str, time_to_str, device_id)
                            print(sql)

                            #sql1 = get_deployment_radar_only_colapsed_query(str(device_id), time_from_str, time_to_str, [device_id])
                            #print(sql1)
                            st = time.time()
                            cur.execute(sql)
                            my_data = cur.fetchall()

                            timestamps, stationary, motion = process_raw_data(my_data)
                            print(type(stationary))
                            # Find threshold above which 20% of points lie
                            AveragePercentSpendsThere = AveragePercentPerLocation[Consolidataed_locations[location]]
                            threshold_high, threshold_low = FindThreshold(stationary, AveragePercentSpendsThere)
                            file_save = f"threshold_graph_{location}.png"
                            title = f"{well_id}_{location}"

                            threshold2, x_percent, y_percent = ShowThresholdGraph(stationary, file_save, threshold_low, threshold_high, title, AveragePercentSpendsThere, location)

                            print(f"Maximum curvature point found at:")
                            print(f"Threshold value: {threshold2:.3f}")
                            print(f"X: {x_percent:.1f}% of range")
                            print(f"Y: {y_percent:.1f}% of points above")

                            ShowArray(stationary, threshold2, filename=f"stationary_{devices_list[device_index][0]}.png", title=f"stationary_{devices_list[device_index][0]}_{devices_list[device_index][2]}", style='line')


                            ##threshold
                            ##presence_mask, baseline, threshold = detect_presence(timestamps, stationary, motion)

                            ### Save visualization to file
                            ##visualize_detection(timestamps, stationary, motion, presence_mask,
                            ##                   baseline, threshold)

                            #cur.execute(sql1)
                            #my_data1 = cur.fetchall()#cur.fetchone()
                            #print(time.time() - st)
                            #if my_data == None or my_data1 == None:
                                #logger.warning(f"No data found for device_id {device_id}")
                            #else:
                                #print(type(my_data))
                                ##minute,
                                ##device_id,
                                ##s_min as radar_s_min,
                                ##s_max as radar_s_max,
                                ##m_max as radar_m_max

                                #values = [tup[1] for tup in my_data]  #10 sec (RAW) data

                                #hist, bins = np.histogram(values, bins=1000, range=(0, 100))
                                #TR, BR = FindZeroIntersection(hist, bins, f'raw_{device_id}_histogram.png', device_id)
                                #if True:#device_id == 560:
                                    #plot(values, filename=f"radar_{device_id}_s28.png", title=f"Radar s28 {device_id}", style='line')
                                    #plot(hist, filename=f"radar_{device_id}_s28_hist.png", title=f"Radar s28 {device_id} histogram", style='line')

                                ##life = [tup[3] - tup[2] + tup[4] for tup in my_data1]
                                #life, average = calculate_life_and_average(my_data1, stdev_range) #5 min data
                                #lhist, lbins = np.histogram(life, bins=1000)
                                #TLIFE, BLIFE = FindZeroIntersection(lhist, lbins, f'life_{device_id}_histogram.png', device_id)

                                #StoreThresholds2DB(device_id, TR, BR, TLIFE, BLIFE)
                                ##for now not needed...
                                ##ahist, abins = np.histogram(average, bins=1000)
                                ##dummy1, dummy = FindZeroIntersection(ahist, abins)
                                #if True:#device_id == 560:
                                    #plot(average, filename=f"average_{device_id}.png", title=f"Average {device_id}", style='line')
                                    #plot(life, filename=f"life_{device_id}.png", title=f"Life {device_id}", style='line')
                                    #plot(lhist, filename=f"life_{device_id}_hist.png", title=f"life {device_id} histogram", style='line')
                                    ##plot(ahist, filename=f"average_{device_id}_hist.png", title=f"average {device_id} histogram", style='line')


                sql = get_deployment_radar_only_colapsed_query(devices_list_str, time_from_str, time_to_str, ids_list)
                print(sql)
                my_data = []
                with get_db_connection() as conn:
                    with conn.cursor() as cur:
                        cur.execute(sql)
                        my_data = cur.fetchall()#cur.fetchone()
                        #print(result)
                        if my_data == None:
                            return False

                fields_n = len(fields)
                stripes = devices_c * fields_n #radar_min and radar_max
                print(my_data)
                base_minute = ConvertToBase(time_from_str, time_zone_s)
                #base_minute = my_data[0][0]# min(record[0] for record in my_data)
                #remember: base_minute is offset (smaller) by numbr of minutes in stdev_range
                st = time.time()
                wave_m = np.zeros((stripes, 1440+2*stdev_range, 1), dtype=np.float32)

                for record in my_data:
                    #(minute,device_id,s28_min,s28_max) = record
                    minute, device_id = record[0:2]
                    values = record[2:]  # All the min/max values
                    x = int((minute - base_minute).total_seconds()/60)

                    device_idx = device_to_index[device_id]
                    #value[0] are mins, value[1] are maxes
                    #when trying to illustrate presence, use s28_max, when absence (night leaving bed) use s28s_min
                    for field_idx, value in enumerate(values):
                        # Calculate y position
                        y = device_idx * fields_n + field_idx
                        wave_m[y, x] = value

                print(time.time()-st)

                #we need to reliably determine presence and LIFE (motion) in every 5 minutes of data...
                #presence is determined by average value being significntly different from last known base
                #last known base is determined by average value during extended periods ( >= H hours) of low stdev (<) while it is determined that:
                #person is moving elsewhere, and only 1 person is determined to be in monitored area.

                #lets calculate stdevs
                for device_index in range(devices_c):
                    y = device_index * fields_n
                    row = wave_m[y]
                    stdevs = np.zeros((1440+2*stdev_range, 1), dtype=np.float32)
                    stdevs, amplitude = CalcStdevs(row, stdev_range, stdevs)
                    wave_m[y+3] = stdevs
                    plot(stdevs, filename=f"radar{device_index}_stdevs.png", title=f"Radar Stedevs {device_index}", style='line')

                minutes = 1440


                device_index = 0
                y = 0
                for device in devices_list:
                    wave = wave_m[y][stdev_range: stdev_range + minutes]
                    plot(wave,
                         filename="radar_wave_min.png",
                         title="Radar Signal Min",
                         style='line')
                    # Create histogram with 1000 bins
                    hist, bins = np.histogram(wave, bins=1000, range=(0, 100))

                    #bin_centers = (bins[:-1] + bins[1:]) / 2
                    hist_line = hist  # These are your y values

                    # Plot with proper axis labels
                    plot(hist_line,
                         filename="radar_histogram_min.png",
                         title="Radar Signal Histogram Min (1000 bins)",
                         style='line')

                    wave = wave_m[y+1]
                    plot(wave,
                         filename="radar_wave_max.png",
                         title="Radar Signal",
                         style='line')
                    # Create histogram with 1000 bins
                    hist, bins = np.histogram(wave, bins=1000, range=(0, 100))

                    #bin_centers = (bins[:-1] + bins[1:]) / 2
                    hist_line = hist  # These are your y values

                    # Plot with proper axis labels
                    plot(hist_line,
                         filename="radar_histogram_max.png",
                         title="Radar Signal Histogram Max(1000 bins)",
                         style='line')

                    print(wave)
                    device_to_index += 1

                    #lets see this map
                    stretch_by = 5
                    arr_stretched = np.zeros((int(stripes*stretch_by), minutes, 3), dtype=np.uint8)  #array to be written as image 3 for RGB channels
                    st = time.time()
                    for yy in range(stripes):
                        rgb_row = []
                        row = wave_m[yy]
                        for x in range(minutes):
                            value = 1280 * row[x] / 100
                            rgb_row.append(BestColor(value))
                        for stretch_index in range(stretch_by):
                            y = yy * stretch_by + stretch_index
                            arr_stretched[y, :] = rgb_row

                    print(time.time()-st)
                    filename = f"{deployment_id}/{deployment_id}_{ddate}_min_max_radar.png"
                    SaveImageInBlob(filename, arr_stretched, [])


                return

            elif function == "request_single_slice":
                deployment_id = form_data.get('deployment_id')
                time_zone_s = GetTimeZoneOfDeployment(deployment_id)
                selected_date = form_data.get('date')
                devices_list = form_data.get('devices_list')
                #devices_list = '[267,560,"?",null,"64B70888F6F0"]'
                #devices_list = '[[267,560,"?",null,"64B70888F6F0"],[268,561,"?",null,"64B70888F6F1"]]'
                sensor_list_loc = [form_data.get('sensor_list')]
                is_nested, device_details = check_and_parse(devices_list)
                if not is_nested:
                    device_ids_list = [device_details[1]]
                    well_ids_list = [device_details[0]]
                else:
                    device_ids_list = list(map(lambda x: x[1], device_details))
                    well_ids_list =list(map(lambda x: x[0], device_details))

                data_type = form_data.get('data_type')
                epoch_from_utc, epoch_to_utc = GetLocalTimeEpochsForDate(selected_date, time_zone_s) #>= #<

                #epoch_to = '1730592010' #smal sample to test
                radar_part = form_data.get('radar_part')
                well_id = well_ids_list[0]
                all_slices = {}
                device_id2_mac = {device_details[1]: device_details[4]}
                for device_id in device_ids_list:
                    device_id2_mac
                    sensor_data = {}
                    for sensor in sensor_list_loc:
                        st = time.time()
                        line_part = ReadSensor(device_id, sensor, epoch_from_utc, epoch_to_utc, data_type, radar_part)
                        window =  sensor_legal_values[sensor][2]
                        #print("@1", time.time() - st)
                        #first = 3300
                        #last = 3400
                        #line_part =  line_part[first:last]
                        line_part_t = []
                        #st = time.time()
                        #line_part_t = [tuple(x[:2]) for x in line_part]
                        #print(time.time() - st)
                        #st = time.time()
                        #line_part_t = list({(dt.timestamp(), value) for dt, value in line_part})
                        #print(time.time() - st)

                        line_part_t = [(x[0].timestamp(), x[1]) for x in line_part]
                        st = time.time()
                        cleaned_values_t = clean_data_pd(line_part_t, window=window, percentile=99)
                        cleaned_values = add_boundary_points(cleaned_values_t, time_zone_s)
                        #print("@2", time.time() - st)

                        #Lets add point in minute 0 and minute 1439

                        #st = time.time()
                        #cleaned_values = clean_data_fast(line_part_t, window=5, threshold=2.0)
                        #print("@3", time.time() - st)

                        sensor_data[sensor] = cleaned_values
                    all_slices[device_id2_mac[device_id]] = sensor_data #use MAC instead of device_id, since device is sending data with MAC only

                    dataa = {}
                    dataa['Function'] = "single_slicedata"
                    dataa['devices_list'] = devices_list
                    dataa['all_slices'] = all_slices
                    dataa['time_zone_st'] = time_zone_s
                    dataa['well_id'] = well_id
                resp.media = package_response(dataa)
                resp.status = falcon.HTTP_200
                #return
            elif function == "get_sensor_bucketed_data_by_room_sensor":
                # Inputs:
                # user_name and token
                # deployment_id - from which report gets deployment set (all rooms and devices) to get timezone
                # date - one day in a format YYYY-MM-DD
                # sensor - temperature/radar/etc.. see full list
                #           (tells what sensor data to be retrieved)
                #           "voc" for all smell use s4 (lower reading is higher smell, max=0 find min for 100%)
                #           "radar" returns s28
                # radar_part - optional and applies only to radar (tells which segment of radar to be retrieved)
                # bucket_size - ['no', '10s', '1m', '5m', '10m', '15m', '30m', '1h']
                # location - room name (has to be unique)
                # data_type - ML
                # Output: son structure with the following info
                # chart_data with rooms : [list]
                deployment_id   = form_data.get('deployment_id')
                selected_date   = form_data.get('date')
                sensor          = form_data.get('sensor') # one sensor
                radar_part      = form_data.get('radar_part')
                buckets         = ['no', '10s', '1m', '5m', '10m', '15m', '30m', '1h']
                bucket_size     = "no" if (result := form_data.get('bucket_size')) in (None, "") else (result.strip() if result.strip() in buckets else "no")
                #bucket_size     = res2 if (res := form_data.get('bucket_size')) is not None and (res2 := str(res).strip()) and res2 in {'no', '10s', '1m', '5m', '10m', '15m', '30m', '1h'} else 'no'
                location        = form_data.get('location')
                data_type       = form_data.get('data_type')

                time_zone_s     = GetTimeZoneOfDeployment(deployment_id)
                epoch_from_utc, epoch_to_utc = GetLocalTimeEpochsForDate(selected_date, time_zone_s)

                # obtain devices_list for deployment_id
                selected_date   = selected_date.replace("_","-")
                devices_list, device_ids =  GetProximityList(deployment_id, epoch_from_utc)
                sensor_data = {}
                units = "°C"
                if "America" in time_zone_s:
                    units = "°F"
                # see https://www.w3schools.com/cssref/css_colors.php
                sensor_props = {"temperature":  ["red", units],
                                "humidity":     ["blue", "%"],
                                "voc":          ["orange", "PPM"],
                                "co2":          ["orange", "PPM"],
                                "pressure":     ["magenta", "Bar"],
                                "radar":        ["cyan", "%"],
                                "light":        ["yellow", "Lux"]}

                current_time_la = datetime.datetime.now(pytz.timezone(time_zone_s))
                formatted_time = current_time_la.strftime('%Y-%m-%dT%H:%M:%S') #"2025-02-06T20:09:00"

                result_dictionary = {
                    "last_report_at": formatted_time,
                    "color": sensor_props[sensor][0] if sensor in s_table else "grey",
                    "units": sensor_props[sensor][1] if sensor in s_table else "?"
                }
                #sensor_mapping = {"co2": "s4", "voc": "s9"}
                #sensor = sensor_mapping.get(sensor, sensor)

                chart_data = []
                # example data in each element of devices_list is (266, 559, 'Bathroom', None, '64B70888FAB0', '["s3_max",12]')
                for well_id, device_id, location_name, description, MAC, radar_threshold_group_st, close_to in devices_list:
                    if  location_name == location:
                        line_part = ReadSensor3(device_id, sensor, epoch_from_utc, epoch_to_utc, data_type, radar_part, bucket_size)
                        window =  sensor_legal_values[sensor][2]
                        line_part_t = []
                        line_part_t = [(x[0].timestamp(), x[1]) for x in line_part]
                        st = time.time()
                        cleaned_values_t = clean_data_pd(line_part_t, window=window, percentile=99)
                        cleaned_values = cleaned_values_t #add_boundary_points(cleaned_values_t, time_zone_s)
                        compressed_readings = convert_timestamps_lc(cleaned_values, time_zone_s)
                        if sensor == "temperature":
                            if units == "°F":#"America" in time_zone_s:
                                compressed_readings = CelsiusToFahrenheitList(compressed_readings)



                        sensor_data[sensor] = compressed_readings
                        chart_data.append({'name': location_name, 'data': compressed_readings})
                result_dictionary['chart_data'] = chart_data
                payload = result_dictionary
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200

            elif function == "get_sensor_data_by_deployment_id":
                # Inputs:
                # user_name and token
                # deployment_id - from which report gets deployment set (all rooms and devices)
                # date - one day in a format YYYY-MM-DD
                # sensor - temperature/radar/etc.. see full list (tells what sensor data to be retrieved)
                # radar_part - optional and applies only to radar (tells which segment of radar to be retrieved)
                # bucket_size - ['no', '10s', '1m', '5m', '10m', '15m', '30m', '1h']
                # data_type - ML
                # Output: son structure with the following info
                # chart_data with rooms : [list]
                deployment_id   = form_data.get('deployment_id')
                time_zone_s     = GetTimeZoneOfDeployment(deployment_id)
                selected_date   = form_data.get('date')
                sensor          = form_data.get('sensor') # one sensor
                radar_part      = form_data.get('radar_part')
                buckets         = ['no', '10s', '1m', '5m', '10m', '15m', '30m', '1h']
                bucket_size     = "no" if (result := form_data.get('bucket_size')) in (None, "") else (result.strip() if result.strip() in buckets else "no")
                #bucket_size     = res2 if (res := form_data.get('bucket_size')) is not None and (res2 := str(res).strip()) and res2 in {'no', '10s', '1m', '5m', '10m', '15m', '30m', '1h'} else 'no'
                data_type       = form_data.get('data_type')

                epoch_from_utc, epoch_to_utc = GetLocalTimeEpochsForDate(selected_date, time_zone_s) #>= #<
                # obtain devices_list for deployment_id
                selected_date   = selected_date.replace("_","-")
                #timee           = LocalDateToUTCEpoch(selected_date, time_zone_s)+5
                devices_list, device_ids =  GetProximityList(deployment_id, epoch_from_utc)
                sensor_data = {}
                # see https://www.w3schools.com/cssref/css_colors.php
                sensor_props = {"temperature":  ["red", "°C"],
                                "humidity":     ["blue", "%"],
                                "voc":          ["orange", "PPM"],
                                "co2":          ["orange", "PPM"],
                                "pressure":     ["magenta", "Bar"],
                                "radar":        ["cyan", "%"],
                                "light":        ["yellow", "Lux"]}
                result_dictionary = {
                    "last_report_at": "2025-02-06T20:09:00",
                    "color": sensor_props[sensor][0] if sensor in s_table else "grey",
                    "units": sensor_props[sensor][1] if sensor in s_table else "?"
                }
                #sensor_mapping = {"co2": "s4", "voc": "s9"}
                #sensor = sensor_mapping.get(sensor, sensor)

                chart_data = []
                for room_details in devices_list:
                    well_id, device_id, location_name, description, MAC, radar_threshold_group_st, close_to = room_details #(266, 559, 'Bathroom', None, '64B70888FAB0', '["s3_max",12]')
                    line_part = ReadSensor3(device_id, sensor, epoch_from_utc, epoch_to_utc, data_type, radar_part, bucket_size)
                    window =  sensor_legal_values[sensor][2]
                    line_part_t = []
                    line_part_t = [(x[0].timestamp(), x[1]) for x in line_part]
                    st = time.time()
                    cleaned_values_t = clean_data_pd(line_part_t, window=window, percentile=99)
                    cleaned_values = add_boundary_points(cleaned_values_t, time_zone_s)
                    compressed_readings = convert_timestamps_lc(cleaned_values, time_zone_s)

                    #compressed_readings =  [(time.strftime("%H:%M", time.gmtime(lst[0][0])), float(sum(t for _, t in lst)/len(lst)))
                                            #for _, lst in ((k, list(g))
                                                #for k, g in itertools.groupby(cleaned_values, key=lambda x: time.gmtime(x[0]).tm_hour))]
                    sensor_data[sensor] = compressed_readings
                    chart_data.append({'name': location_name,
                                       'data': compressed_readings})
                result_dictionary['chart_data'] = chart_data
                #all_slices[device_id2_mac[device_id]] = sensor_data #use MAC instead of device_id, since device is sending data with MAC only
                #is_neste, device_details = check_and_parse(devices_list)
                #if not is_nested:
                    #device_ids_list = [device_details[1]]
                    #well_ids_list = [device_details[0]]
                #else:
                    #device_ids_list = list(map(lambda x: x[1], device_details))
                    #well_ids_list =list(map(lambda x: x[0], device_details))
                #well_id = well_ids_list[0]
                #all_slices = {}
                #device_id2_mac = {device_details[1]: device_details[4]}
                #for device_id in device_ids_list:
                    #device_id2_mac
                    #sensor_data = {}
                    #for sensor in sensor_list_loc:
                        #st = time.time()
                        #line_part = ReadSensor(device_id, sensor, epoch_from_utc, epoch_to_utc, data_type, radar_part)
                        #window =  sensor_legal_values[sensor][2]
                        #line_part_t = []
                        #line_part_t = [(x[0].timestamp(), x[1]) for x in line_part]
                        #st = time.time()
                        #cleaned_values_t = clean_data_pd(line_part_t, window=window, percentile=99)
                        #cleaned_values = add_boundary_points(cleaned_values_t, time_zone_s)
                        #sensor_data[sensor] = cleaned_values
                    #all_slices[device_id2_mac[device_id]] = sensor_data #use MAC instead of device_id, since device is sending data with MAC only

                    #dataa = {}
                    #dataa['Function'] = "single_slicedata"
                    #dataa['devices_list'] = devices_list
                    #dataa['all_slices'] = all_slices
                    #dataa['time_zone_st'] = time_zone_s
                    #dataa['well_id'] = well_id
                #resp.media = package_response(dataa)
                #resp.status = falcon.HTTP_200
                result_dictionary2 = {
                  "alert_text": "No alert",
                  "alert_color": "bg-green-100 text-green-700",
                  "last_report_at": "ISO TIMESTAMP",
                  "chart_data": [
                      {
                          "rooms": [
                          { "name": "Bathroom",
                            "data": [
                                {"title": "12AM","value": 20},
                                {"title": "01AM","value": 20},
                                {"title": "02AM","value": 26},
                                {"title": "03AM","value": 16},
                                {"title": "04AM","value": 27},
                                {"title": "05AM","value": 23},
                                {"title": "06AM","value": 26},
                                {"title": "07AM","value": 17},
                                {"title": "08AM","value": 18},
                                {"title": "09AM","value": 21},
                                {"title": "10AM","value": 28},
                                {"title": "11AM","value": 24},
                                {"title": "12PM","value": 18},
                                {"title": "01PM","value": 27},
                                {"title": "02PM","value": 27},
                                {"title": "03PM","value": 19},
                                {"title": "04PM","value": 0},
                                {"title": "05PM","value": 0},
                                {"title": "06PM","value": 0},
                                {"title": "07PM","value": 0},
                                {"title": "08PM","value": 0},
                                {"title": "09PM","value": 0},
                                {"title": "10PM","value": 0},
                                {"title": "11PM","value": 0}
                            ]
                          },
                          { "name": "Kitchen",
                              "data": [
                                  {"title": "00AM","value": 19},
                                  {"title": "01AM","value": 10},
                                  {"title": "02AM","value": 8},
                                  {"title": "03AM","value": 14},
                                  {"title": "04AM","value": 20},
                                  {"title": "05AM","value": 8},
                                  {"title": "06AM","value": 7},
                                  {"title": "07AM","value": 17},
                                  {"title": "08AM","value": 3},
                                  {"title": "09AM","value": 19},
                                  {"title": "10AM","value": 4},
                                  {"title": "11AM","value": 6},
                                  {"title": "12PM","value": 4},
                                  {"title": "01PM","value": 14},
                                  {"title": "02PM","value": 17},
                                  {"title": "03PM","value": 20},
                                  {"title": "04PM","value": 19},
                                  {"title": "05PM","value": 15},
                                  {"title": "06PM","value": 5},
                                  {"title": "07PM","value": 19},
                                  {"title": "08PM","value": 3},
                                  {"title": "09PM","value": 30},
                                  {"title": "10PM","value": 1},
                                  {"title": "11PM","value": 12 }
                              ]
                          },
                          { "name": "Living Room",
                              "data": [
                                  {"title": "00AM","value": 25},
                                  {"title": "01AM","value": 24},
                                  {"title": "02AM","value": 19},
                                  {"title": "03AM","value": 20},
                                  {"title": "04AM","value": 22},
                                  {"title": "05AM","value": 20},
                                  {"title": "06AM","value": 11},
                                  {"title": "07AM","value": 5},
                                  {"title": "08AM","value": 16},
                                  {"title": "09AM","value": 22},
                                  {"title": "10AM","value": 23},
                                  {"title": "11AM","value": 14},
                                  {"title": "12PM","value": 0},
                                  {"title": "01PM","value": 7},
                                  {"title": "02PM","value": 25},
                                  {"title": "03PM","value": 29},
                                  {"title": "04PM","value": 23},
                                  {"title": "05PM","value": 27},
                                  {"title": "06PM","value": 27},
                                  {"title": "07PM","value": 20},
                                  {"title": "08PM","value": 2},
                                  {"title": "09PM","value": 24},
                                  {"title": "10PM","value": 21},
                                  {"title": "11PM","value": 14 }
                              ]
                          }
                          ]
                      }
                    ]
                }
                payload = result_dictionary
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200
                #AddToLog(payload)
                #return
            elif function == "request_device_slice":
                deployment_id = form_data.get('deployment_id')
                time_zone_s = GetTimeZoneOfDeployment(deployment_id)
                epoch_from_utc = form_data.get('epoch_from')
                epoch_to_utc = form_data.get('epoch_to')
                device_id = form_data.get('device_id')
                well_id = form_data.get('well_id')
                MAC = form_data.get('MAC')
                sensor_list_loc = form_data.get('sensors_list')
                sensor_list = sensor_list_loc.split(",")
                device_ids_list = [device_id]
                well_ids_list = [well_id]
                maps_dates, positions_list = GetDeploymentDatesBoth(deployment_id)

                data_type = "RL"
                #epoch_from_utc, epoch_to_utc = GetLocalTimeEpochsForDate(selected_date, time_zone_s) #>= #<

                #epoch_to = '1730592010' #smal sample to test
                radar_part = form_data.get('radar_part')
                well_id = well_ids_list[0]
                all_slices = {}
                #device_id2_mac = {device_details[1]: device_details[4]}
                for device_id in device_ids_list:
                    #device_id2_mac
                    sensor_data = {}
                    for sensor in sensor_list:
                        st = time.time()
                        line_part = ReadSensor(device_id, sensor, epoch_from_utc, epoch_to_utc, data_type, radar_part)
                        window =  sensor_legal_values[sensor][2]
                        #print("@1", time.time() - st)
                        #first = 3300
                        #last = 3400
                        #line_part =  line_part[first:last]
                        line_part_t = []
                        #st = time.time()
                        #line_part_t = [tuple(x[:2]) for x in line_part]
                        #print(time.time() - st)
                        #st = time.time()
                        #line_part_t = list({(dt.timestamp(), value) for dt, value in line_part})
                        #print(time.time() - st)

                        line_part_t = [(x[0].timestamp(), x[1]) for x in line_part]
                        st = time.time()
                        cleaned_values_t = clean_data_pd(line_part_t, window=window, percentile=99)
                        #cleaned_values = cleaned_values_t #add_boundary_points(cleaned_values_t, time_zone_s)
                        #print("@2", time.time() - st)

                        #Lets add point in minute 0 and minute 1439

                        #st = time.time()
                        #cleaned_values = clean_data_fast(line_part_t, window=5, threshold=2.0)
                        #print("@3", time.time() - st)
                        cleaned_values = ScaleToCommon(cleaned_values_t, sensor)
                        sensor_data[sensor] = cleaned_values
                    all_slices[device_id] = sensor_data

                    dataa = {}
                    dataa['Function'] = "device_slicedata"
                    dataa['all_slices'] = all_slices
                    dataa['time_zone_st'] = time_zone_s
                    dataa['proximity'] = positions_list
                    dataa['well_id'] = well_id
                    dataa['MAC'] = MAC
                resp.media = package_response(dataa)
                resp.status = falcon.HTTP_200
                #return
            elif function == "request_single_radar_slice":
                deployment_id = form_data.get('deployment_id')
                time_zone_s = GetTimeZoneOfDeployment(deployment_id)
                selected_date = form_data.get('date')
                devices_list = form_data.get('devices_list')
                ctrl_key_state = form_data.get('ctrl_key_state')
                alt_key_state = form_data.get('alt_key_state')
                #devices_list = '[267,560,"?",null,"64B70888F6F0"]'
                #devices_list = '[[267,560,"?",null,"64B70888F6F0"],[268,561,"?",null,"64B70888F6F1"]]'
                sensor_index_list = [form_data.get('sensor_index_list')]
                is_nested, device_details = check_and_parse(devices_list)
                if not is_nested:
                    device_ids_list = [device_details[1]]
                    well_ids_list = [device_details[0]]
                else:
                    device_ids_list = list(map(lambda x: x[1], device_details))
                    well_ids_list =list(map(lambda x: x[0], device_details))

                epoch_from_utc, epoch_to_utc = GetLocalTimeEpochsForDate(selected_date, time_zone_s) #>= #<

                #epoch_to = '1730592010' #smal sample to test
                radar_part = form_data.get('radar_part')
                well_id = well_ids_list[0]
                all_slices = {}
                device_id2_mac = {device_details[1]: device_details[4]}
                for device_id in device_ids_list:
                    device_id2_mac
                    sensor_data = {}
                    for sensor_index in sensor_index_list:
                        st = time.time()
                        sensor = ["m0", "m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8", "m08_max", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s28_max", "s28_min"][int(sensor_index)]

                        line_part = ReadRadarDetail(device_id, sensor, epoch_from_utc, epoch_to_utc, alt_key_state)
                        window =  sensor_legal_values["radar"][2]

                        line_part_t = [(x[0].timestamp(), x[1]) for x in line_part]
                        st = time.time()
                        cleaned_values_t = clean_data_pd(line_part_t, window=window, percentile=99)
                        cleaned_values = add_boundary_points(cleaned_values_t, time_zone_s)
                        if len(sensor) < 4:
                            sensor_data[sensor+"_max"] = cleaned_values
                        else:
                            sensor_data[sensor] = cleaned_values
                    all_slices[device_id2_mac[device_id]] = sensor_data #use MAC instead of device_id, since device is sending data with MAC only

                    dataa = {}
                    dataa['Function'] = "single_slicedata"
                    dataa['devices_list'] = devices_list
                    dataa['all_slices'] = all_slices
                    dataa['time_zone_st'] = time_zone_s
                    dataa['well_id'] = well_id
                resp.media = package_response(dataa)
                resp.status = falcon.HTTP_200
            elif function == "get_deployment":
                blob_data = read_file("deployment.html")
                deployment_id = form_data.get('deployment_id')
                #lets update "Deployments" select
                users = GetUsersFromDeployments(privileges)
                blob_data = UpdateDeploymentsSelector(blob_data, users, False, deployment_id)

                resp.content_type = "text/html"
                resp.text = blob_data
                return
            elif function == "get_deployment_j":
                deployment_id = form_data.get('deployment_id')
                time_zone_st = GetTimeZoneOfDeployment(deployment_id)
                date = form_data.get('date')
                if date == None:

                    # Get today's date
                    local_timezone = pytz.timezone(time_zone_st)  # Replace with your local timezone
                    date = datetime.datetime.now(local_timezone).strftime('%Y-%m-%d')

                #epoch_from_utc =  int(datetime.datetime.strptime(date, "%Y-%m-%d").timestamp())
                #devices_list, device_ids =  GetProximityList(deployment_id, epoch_from_utc)

                dataa = {}
                dataa['Function'] = "deployment_details"
                if privileges == "-1":
                    deployment = DeploymentDetails(deployment_id)
                    dataa['deployment_details'] = deployment
                else:
                    privileges = privileges.split(",")
                    if deployment_id in privileges:
                        deployment = DeploymentDetails(deployment_id)
                        dataa['deployment_details'] = deployment


                resp.media = package_response(dataa)
                resp.status = falcon.HTTP_200

                return
            elif function == "get_beneficiary":
                user_id = form_data.get('user_id')
                all_beneficiaries = ListBeneficiaries(privileges, user_id)
                beneficiaries_list = []
                for beneficiary_temp in all_beneficiaries:
                    beneficiaries_list.append(str(beneficiary_temp[0]))

                dataa = {}
                dataa['Function'] = "beneficiary_details"
                if user_id in beneficiaries_list:
                    beneficiary = UserDetails(user_id)
                    #lets remove fields not relevant for beneficiary
                    try:
                        del beneficiary['time_edit']
                    except:
                        pass

                    try:
                        del beneficiary['user_edit']
                    except:
                        pass

                    try:
                        del beneficiary['access_to_deployments']
                    except:
                        pass
                dataa['beneficiary_details'] = beneficiary
                resp.media = package_response(dataa)
                resp.status = falcon.HTTP_200
                return
            elif function == "get_caretaker":

                user_name = form_data.get('user_name')


                all_caretakers = ListCaretakers(privileges, user_name)
                if len(all_caretakers) > 1:
                    user_id = form_data.get('user_id')
                else:
                    user_id = str(all_caretakers[0][0])

                caretakers_list = []
                for caretakers_temp in all_caretakers:
                    caretakers_list.append(str(caretakers_temp[0]))

                dataa = {}
                dataa['Function'] = "caretaker_details"
                if user_id in caretakers_list:
                    caretaker = UserDetails(user_id)
                    #lets remove fields not relevant for beneficiary
                    try:
                        del caretaker['time_edit']
                    except:
                        pass

                    try:
                        del caretaker['user_edit']
                    except:
                        pass

                dataa['caretaker_details'] = caretaker
                resp.media = package_response(dataa)
                resp.status = falcon.HTTP_200
                return

            elif function == "get_device":
                device_id = form_data.get('device_id')
                devices = GetVisibleDevices(privileges)
                dataa = {}
                dataa['Function'] = "device_details"
                dataa['device_details'] = []
                if privileges == "-1":
                    device_det = GetDeviceDetails(device_id)
                    if device_det['radar_threshold'] == None or device_det['radar_threshold'] == "":
                        device_det['radar_threshold'] = '["s3_max",12]'
                    dataa['device_details'] = device_det
                else:
                    devices_list = []
                    for device_id_temp in devices:
                        devices_list.append(str(device_id_temp[0]))

                    if device_id in devices_list:
                        device_det = GetDeviceDetailsSingle(device_id)
                        if device_det['radar_threshold'] == None or device_det['radar_threshold'] == "":
                            device_det['radar_threshold'] = '["s3_max",12]'


                        dataa['device_details'] = device_det


                resp.media = package_response(dataa)
                resp.status = falcon.HTTP_200

                return
            elif function == "request_deployment_map_new":
                st = time.time()
                print(f"$0 ----{time.time() - st}")
                deployment_id = form_data.get('deployment_id')
                map_type = form_data.get('map_type')
                print(f"$1 ----{time.time() - st}")
                maps_dates, positions_list = GetDeploymentDatesBoth(deployment_id)
                print(f"$2 ----{time.time() - st}")
                datee = form_data.get('date')
                if maps_dates != []:

                    if datee == "2022-4-2": #that one is default in HTML so disregard
                        datee = maps_dates[0]

                    locations_desc_map = {}
                    for details in positions_list:
                        well_id = details[0]
                        location = details[2]
                        if details[3] != None and details[3] != "":
                            location = location +" "+ details[3]

                        if details[6] != None and details[6] != "":
                            location = location +" "+ details[6]

                        MAC = details[4]
                        locations_desc_map[well_id] = location
                    print(f"$3 ----{time.time() - st}")

                    dataa = {}
                    dataa['Function'] = "deployments_maps_report"
                    dataa['proximity'] = positions_list
                    maps_dates.sort(reverse = True)
                    dataa['maps_dates'] = maps_dates
                    dataa['device_count'] = len(positions_list)
                    dataa['map_type'] = map_type

                    #MACs_list = GetMACsListSimple(positions_list)
                    #MACs_map = {}

                    #for details in positions_list:
                    #    id = details[0]
                    #    MAC = details[3]
                    #    MACs_map[id] = MAC
                    #for i in range(len(MACs_list)):
                    #  MACs_map[devices_list[i]] = MACs_list[i][0]

                    id = positions_list[0][0]
                    #dataa['MACs_map'] = MACs_map
                    dataa['locations_desc_map'] = locations_desc_map
                    #proximity_list = proximity.split(",")
                    print(f"$4 ----{time.time() - st}")

                    if id < 200:
                        checkmarks_string = 'T><input checked type="checkbox" id="t-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'H><input type="checkbox" id="h-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'P><input type="checkbox" id="p-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'C><input type="checkbox" id="c-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'V><input type="checkbox" id="v-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'L><input type="checkbox" id="l-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'R><input type="checkbox" id="r-check" value="0" onchange="UpdateSelections();"/><br>'
                    else: #>200 = ["Temperature", "Humidity", "Pressure", "Light", "Radar", "VOC"]

                        checkmarks_string = 'T><input checked type="checkbox" id="t-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'H><input type="checkbox" id="h-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'P><input type="checkbox" id="p-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'L><input type="checkbox" id="l-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'R><input type="checkbox" id="r-check" value="0" onchange="UpdateSelections();"/>\n'

                        checkmarks_string = checkmarks_string + 'S0><input type="checkbox" id="v0-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'S1><input type="checkbox" id="v1-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'S2><input type="checkbox" id="v2-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'S3><input type="checkbox" id="v3-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'S4><input type="checkbox" id="v4-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'S5><input type="checkbox" id="v5-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'S6><input type="checkbox" id="v6-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'S7><input type="checkbox" id="v7-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'S8><input type="checkbox" id="v8-check" value="0" onchange="UpdateSelections();"/>\n'
                        checkmarks_string = checkmarks_string + 'S9><input type="checkbox" id="v9-check" value="0" onchange="UpdateSelections();"/><br>'

                    checked_or_not = " checked"

                    for index in range(len(positions_list)):
                        details = positions_list[index]
                        device_id = details[0]
                        location = details[2]
                        if details[3] != None and details[3] != "":
                            location = location + " " + details[3]

                        if details[6] != None and details[6] != "":
                            location = location + " " + details[6]

                        checkmarks_string = checkmarks_string + str(device_id) + '><input'+checked_or_not+' type="checkbox" id="device_check'+str(index)+'" value="0"  title="'+location+'" onchange="UpdateSelections();" />\n'
                        checked_or_not = ''

                    print(f"$5 ----{time.time() - st}")

                    dataa['checkmarks'] = checkmarks_string
                    resp.media = package_response(dataa)
                    resp.status = falcon.HTTP_200

            elif function == "request_proximity":
                deployment = form_data.get('deployment_id')
                timee = form_data.get('time')
                #timee = StringToEpoch(datee)
                #print(deployment, timee)
                well_ids, device_ids = GetProximityList(deployment, timee)
                #print(proximity)
                dataa = {}
                dataa['Function'] = "proximity_report"
                if len(well_ids) > 0:
                    dataa['proximity'] = well_ids
                else:
                    dataa['proximity'] = []

                resp.media = package_response(dataa)
                resp.status = falcon.HTTP_200
            elif function == "request_devices":
                deployment_id = form_data.get('deployment_id')
                group_id = form_data.get('group_id')
                location = form_data.get('location')
                if  location == "0":
                    location = "All"
                is_fresh = form_data.get('is_fresh')
                matching_devices = GetMatchingDevices(privileges, group_id, deployment_id, location)
                dataa = {}
                dataa['Function'] = "devices_report"
                if len(matching_devices) > 0:
                    dataa['devices'] = matching_devices
                else:
                    dataa['devices'] = []

                resp.media = package_response(dataa)
                resp.status = falcon.HTTP_200

            elif function == "device_form":
                editing_device_id = form_data.get('editing_device_id')

                ok = StoreDevice2DB(form_data, editing_device_id)
                if ok == 1:
                    payload = {'ok': ok}
                    resp.media = package_response(payload)
                    resp.status = falcon.HTTP_200
                    return
                else:
                    payload = {'ok': ok, 'error': debug_string}
                    resp.media = package_response(payload)
                    resp.status = falcon.HTTP_200
                    return
            elif function == "device_delete":

                #check if admin!

                ok = DeleteRecordFromDB(form_data)

                payload = {'ok': ok}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200
                return

            elif function == "get_raw_data":
                container = GetReference("/MAC")
                MAC = req_dict["MAC"][0]
                sensor = req_dict["sensor"][0]
                if "part" in req_dict:
                    part = req_dict["part"][0]
                else:
                    part = ""
                from_time = req_dict["from_time"][0]
                to_time = req_dict["to_time"][0]
                timezone_str = req_dict["tzone"][0]
                AddToLog("get_raw_data:" + str(MAC) +","+ str(sensor) + "," + str(from_time) + "," + str(to_time) + "," + part+ "," + timezone_str)
                #raw_data = GetRawSensorData(container, MAC, sensor, from_time, to_time, timezone_str)
                raw_data = GetRawSensorDataFromBlobStorage(MAC, sensor, part, from_time, to_time, timezone_str)
                data_payload = {'raw_data': raw_data}
                resp.media = package_response(data_payload)
                resp.status = falcon.HTTP_200
                return

            elif function == "get_candle_data":
                container = GetReference("/MAC")
                MAC = req_dict["MAC"][0]
                sensor = req_dict["sensor"][0]
                from_time = req_dict["from_time"][0]
                to_time = req_dict["to_time"][0]
                part = req_dict["part"][0]
                tzone = req_dict["tzone"][0]
                AddToLog(str(req_dict))
                candle_data = GetCandleSensorData(container, MAC, sensor, from_time, to_time, part, tzone)
                data_payload = {'candle_data': candle_data}
                resp.media = package_response(data_payload)
                resp.status = falcon.HTTP_200
                return

            elif function == "deployment_form":
                editing_deployment_id = form_data.get('editing_deployment_id')

                ok = StoreDeployment2DB(form_data, editing_deployment_id)
                if ok == 1:
                    payload = {'ok': ok}
                    resp.media = package_response(payload)
                    resp.status = falcon.HTTP_200
                    return
                else:
                    payload = {'ok': ok, 'error': debug_string}
                    resp.media = package_response(payload)
                    resp.status = falcon.HTTP_200
                    return
            elif function == "deployment_delete":
                ok = DeleteRecordFromDB(form_data)

                payload = {'ok': ok}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200
                return

            elif function == "deployments_list":
                result_list = []
                first_s = form_data.get('first')
                last_s = form_data.get('last')

                try:
                    first = int(first_s)
                except ValueError:
                    first = 0

                try:
                    last = int(last_s)
                except ValueError:
                    last = 1000000

                user_id = form_data.get('user_id')
                all_deployments = ListDeployments(privileges, user_id)

                cnt = 0

                for deployment in all_deployments:
                    cnt += 1
                    if cnt >= first:
                        caretaker_min_object = {"deployment_id": deployment['deployment_id'], "email": user_id_2_user[deployment['beneficiary_id']][3], "first_name": user_id_2_user[deployment['beneficiary_id']][5], "last_name": user_id_2_user[deployment['beneficiary_id']][6]}
                        result_list.append(caretaker_min_object)
                    if cnt > last:
                        break

                payload = {'result_list': result_list}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200
                return
            elif function == "device_list":
                result_list = []
                first_s = form_data.get('first')
                last_s = form_data.get('last')

                try:
                    first = int(first_s)
                except ValueError:
                    first = 0

                try:
                    last = int(last_s)
                except ValueError:
                    last = 1000000

                #user_id = form_data.get('user_id')

                devices = GetVisibleDevices(privileges)

                payload = {'result_list': devices}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200
                return

            elif function == "device_list_by_deployment":
                result_list = []
                first_s = form_data.get('first')
                last_s = form_data.get('last')
                deployment_id = form_data.get('deployment_id')
                try:
                    first = int(first_s)
                except ValueError:
                    first = 0

                try:
                    last = int(last_s)
                except ValueError:
                    last = 1000000


                if privileges == "-1":
                    devices = GetVisibleDevices(deployment_id)
                else:
                    privileges = privileges.split(",")
                    if deployment_id in privileges:
                        devices = GetVisibleDevices(deployment_id)

                payload = {'result_list': devices}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200
                return

            elif function == "device_list_4_gui":
                result_list = []
                deploymentData = []
                deviceData = []
                macs_list = []
                user_id = GetUserId(user_name)
                all_deployments = ListDeployments(privileges, user_id)
                #{'deployment_id': 21, 'beneficiary_id': 25, 'caretaker_id': 1, 'owner_id': 1, 'installer_id': 1, 'address_street': '661 Encore Way', 'address_city': 'San Jose', 'address_zip': '95134', 'address_state': 'CA', 'address_country': 'USA', 'devices': '["64B70888FAB0","64B70888F860","64B70888F6F0","64B708896BDC","64B708897428","64B70888FA84","64B70889062C"]', 'wifis': '', 'persons': 1, 'gender': 1, 'race': 1, 'born': 1940, 'pets': 0, 'time_zone': 'America/Los_Angeles'}
                MAC2Deployment = {}

                for deployment in all_deployments:
                    beneficiary_id = deployment['beneficiary_id']
                    user = GetNameFromUserId(beneficiary_id)
                    name = f"{user[1]} {user[2]}"
                    deploymentData.append({'deployment_id': str(deployment['deployment_id']), 'name': name})
                    devices = deployment['devices']
                    if devices != None:

                        devices_list = ToList(devices)
                        for device in devices_list:
                            macs_list.append(device)
                            MAC2Deployment[device] = deployment['deployment_id']
                            #deviceData.append({'well_id': device[0], 'mac': device[1]})

                deployment_id_list = []
                deviceData = []

                #row_data = [device_id, well_id, mac, last_message_epoch, location_names[location_id], description, deployment_ids[cnt][0]]

                with get_db_connection() as conn:
                    with conn.cursor() as cur:
                        device_ids, device_list = MACsToWellIds(cur, macs_list)


                for device in device_list:
                    if MAC2Deployment[device[4]] != "":
                        deviceData.append({'well_id': device[0], 'mac': device[4], 'room_name': device[2], 'deployment_id': MAC2Deployment[device[4]]})

                #deploymentData = [{'deployment_id': '21', 'name': 'Robert Zmrzli House'}, {'deployment_id': '36', 'name': 'Fred Zmrzli Apartment'}]
                #deviceData = [{ 'well_id': '300', 'mac': '64B70888F6F0', 'room_name': 'Living Room', 'deployment_id': '21' }, { 'well_id': '301', 'mac': '64B70888F6F1', 'room_name': 'Bathroom Main', 'deployment_id': '36' }]

                payload = {
                    'status': "success", 'deploymentData': deploymentData, 'deviceData': deviceData
                }

                logger.debug(f"device_list_4_gui------ {payload} ------------------------------------------")

                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200
                return

            elif function == "caretaker_form":
                editing_user_id = form_data.get('editing_user_id')
                email = form_data.get('email')
                user_id = form_data.get('user_id')
                if "@" not in email:
                    resp.media = package_response("Missing or illegal 'email' parameter", HTTP_400)
                    return

                print(privileges)
                if privileges == "-1":
                    ok = StoreCaretaker2DB(form_data, editing_user_id, user_id)
                    if ok == 1:
                        payload = {'ok': ok}
                        resp.media = package_response(payload)
                        resp.status = falcon.HTTP_200
                        return
                    else:
                        payload = {'ok': ok, 'error': debug_string}
                        resp.media = package_response(payload)
                        resp.status = falcon.HTTP_200
                        return

                elif "-1" in privileges:
                    payload = {'ok': 0, 'error': "Not allowed!"}
                    resp.media = package_response(payload)
                    resp.status = falcon.HTTP_200
                    return


            elif function == "caretaker_delete":
                if privileges == "-1":
                    ok = DeleteRecordFromDB(form_data)
                else:
                    ok = 0
                AddToLog(ok)
                payload = {'ok': ok}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200
                return

            elif function == "caretakers_list":
                result_list = []
                first_s = form_data.get('first')
                last_s = form_data.get('last')

                try:
                    first = int(first_s)
                except ValueError:
                    first = 0

                try:
                    last = int(last_s)
                except ValueError:
                    last = 1000000

                if privileges == "-1":
                    all_caretakers = ListCaretakers(privileges, user_name)

                    cnt = 0

                    for caretaker in all_caretakers:
                        cnt += 1
                        if cnt >= first:
                            caretaker_min_object = {"user_id": caretaker[0], "email": caretaker[3], "first_name": caretaker[5], "last_name": caretaker[6]}
                            result_list.append(caretaker_min_object)
                        if cnt > last:
                            break
                elif "-1" in privileges:
                    all_caretakers = ListCaretakers(privileges, user_name)

                    cnt = 0

                    for caretaker in all_caretakers:
                        cnt += 1
                        if cnt >= first:
                            caretaker_min_object = {"user_id": caretaker[0], "email": caretaker[3], "first_name": caretaker[5], "last_name": caretaker[6]}
                            result_list.append(caretaker_min_object)
                        if cnt > last:
                            break
                payload = {'result_list': result_list}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200
                return

            elif function == "beneficiary_form":
                editing_user_id = form_data.get('editing_user_id')
                email = form_data.get('email')
                user_id = GetUserId(user_name)
                if "@" in email:
                    ok, error_string = StoreBeneficiary2DB(form_data, editing_user_id, user_id)
                    if ok == 1:
                        payload = {'ok': ok}
                        resp.media = package_response(payload)
                        resp.status = falcon.HTTP_200
                        return
                    else:
                        payload = {'ok': ok, 'error': error_string}
                        resp.media = package_response(payload)
                        resp.status = falcon.HTTP_200
                        return
                else:
                    payload = {'ok': ok}
                    resp.media = package_response(payload)
                    resp.status = falcon.HTTP_200
                    return

            elif function == "beneficiary_delete":


                ok = DeleteRecordFromDB(form_data)

                payload = {'ok': ok}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200
                return

            elif function == "beneficiaries_list":
                result_list = []
                first_s = form_data.get('first')
                last_s = form_data.get('last')

                try:
                    first = int(first_s)
                except ValueError:
                    first = 0

                try:
                    last = int(last_s)
                except ValueError:
                    last = 1000000

                user_id = form_data.get('user_id')
                all_beneficiaries = ListBeneficiaries(privileges, user_id)

                cnt = 0

                for beneficiary in all_beneficiaries:
                    cnt += 1
                    if cnt >= first:
                        beneficiary_min_object = {"user_id": beneficiary[0], "email": beneficiary[3], "first_name": beneficiary[5], "last_name": beneficiary[6]}
                        result_list.append(beneficiary_min_object)
                    if cnt > last:
                        break

                payload = {'result_list': result_list}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200
                return

            elif function == "activities_report_details":
                deployment_id = form_data.get('deployment_id')
                result_dictionary = {
                  "alert_text": "No alert",
                  "alert_color": "bg-green-100 text-green-700",
                  "chart_data": [
                    {
                      "name": "Weekly",
                      "rooms": [
                        {
                          "name": "Bathroom",
                          "color": "purple",
                          "data": [
                            { "title": "Monday", "events": 186, "hours": 80 },
                            { "title": "Tuesday", "events": 305, "hours": 200 },
                            { "title": "Wednesday", "events": 237, "hours": 120 },
                            { "title": "Thursday", "events": 73, "hours": 190 },
                            { "title": "Friday", "events": 209, "hours": 130 },
                            { "title": "Saturday", "events": 214, "hours": 140 },
                            { "title": "Sunday", "events": 150, "hours": 100 }
                          ]
                        },
                        {
                          "name": "Bedroom",
                          "color": "#3b82f6",
                          "data": [
                            { "title": "Monday", "events": 186, "hours": 80 },
                            { "title": "Tuesday", "events": 305, "hours": 200 },
                            { "title": "Wednesday", "events": 237, "hours": 120 },
                            { "title": "Thursday", "events": 73, "hours": 190 },
                            { "title": "Friday", "events": 209, "hours": 130 },
                            { "title": "Saturday", "events": 214, "hours": 140 },
                            { "title": "Sunday", "events": 150, "hours": 100 }
                          ]
                        },
                        {
                          "name": "Kitchen",
                          "color": "orange",
                          "data": [
                            { "title": "Monday", "events": 186, "hours": 80 },
                            { "title": "Tuesday", "events": 305, "hours": 200 },
                            { "title": "Wednesday", "events": 237, "hours": 120 },
                            { "title": "Thursday", "events": 73, "hours": 190 },
                            { "title": "Friday", "events": 209, "hours": 130 },
                            { "title": "Saturday", "events": 214, "hours": 140 },
                            { "title": "Sunday", "events": 150, "hours": 100 }
                          ]
                        },
                        {
                          "name": "Other",
                          "color": "hotpink",
                          "data": [
                            { "title": "Monday", "events": 186, "hours": 80 },
                            { "title": "Tuesday", "events": 305, "hours": 200 },
                            { "title": "Wednesday", "events": 237, "hours": 120 },
                            { "title": "Thursday", "events": 73, "hours": 190 },
                            { "title": "Friday", "events": 209, "hours": 130 },
                            { "title": "Saturday", "events": 214, "hours": 140 },
                            { "title": "Sunday", "events": 150, "hours": 100 }
                          ]
                        }
                      ]
                    },
                    {
                      "name": "Monthly",
                      "rooms": [
                        {
                          "name": "Bathroom",
                          "color": "purple",
                          "data": [
                            { "title": "01", "events": 67, "hours": 45 },
                            { "title": "02", "events": 97, "hours": 67 },
                            { "title": "03", "events": 87, "hours": 23 },
                            { "title": "04", "events": 42, "hours": 12 },
                            { "title": "05", "events": 64, "hours": 48 },
                            { "title": "06", "events": 53, "hours": 34 },
                            { "title": "07", "events": 75, "hours": 23 },
                            { "title": "08", "events": 45, "hours": 56 },
                            { "title": "09", "events": 85, "hours": 47 },
                            { "title": "10", "events": 34, "hours": 29 },
                            { "title": "11", "events": 49, "hours": 30 },
                            { "title": "12", "events": 62, "hours": 33 },
                            { "title": "13", "events": 75, "hours": 44 },
                            { "title": "14", "events": 88, "hours": 57 },
                            { "title": "15", "events": 94, "hours": 65 },
                            { "title": "16", "events": 45, "hours": 21 },
                            { "title": "17", "events": 76, "hours": 54 },
                            { "title": "18", "events": 85, "hours": 62 },
                            { "title": "19", "events": 43, "hours": 28 },
                            { "title": "20", "events": 59, "hours": 34 },
                            { "title": "21", "events": 78, "hours": 56 },
                            { "title": "22", "events": 64, "hours": 39 },
                            { "title": "23", "events": 93, "hours": 72 },
                            { "title": "24", "events": 52, "hours": 28 },
                            { "title": "25", "events": 71, "hours": 48 },
                            { "title": "26", "events": 85, "hours": 63 }
                          ]
                        },
                        {
                          "name": "Bedroom",
                          "color": "#3b82f6",
                          "data": [
                            { "title": "01", "events": 61, "hours": 42 },
                            { "title": "02", "events": 72, "hours": 36 },
                            { "title": "03", "events": 94, "hours": 49 },
                            { "title": "04", "events": 67, "hours": 59 },
                            { "title": "05", "events": 54, "hours": 20 },
                            { "title": "06", "events": 77, "hours": 64 },
                            { "title": "07", "events": 81, "hours": 70 },
                            { "title": "08", "events": 53, "hours": 25 },
                            { "title": "09", "events": 79, "hours": 42 },
                            { "title": "10", "events": 84, "hours": 65 },
                            { "title": "11", "events": 62, "hours": 54 },
                            { "title": "12", "events": 45, "hours": 23 },
                            { "title": "13", "events": 88, "hours": 71 },
                            { "title": "14", "events": 74, "hours": 44 },
                            { "title": "15", "events": 91, "hours": 59 },
                            { "title": "16", "events": 46, "hours": 31 },
                            { "title": "17", "events": 73, "hours": 40 },
                            { "title": "18", "events": 85, "hours": 63 },
                            { "title": "19", "events": 78, "hours": 66 },
                            { "title": "20", "events": 66, "hours": 42 },
                            { "title": "21", "events": 95, "hours": 78 },
                            { "title": "22", "events": 57, "hours": 39 },
                            { "title": "23", "events": 72, "hours": 48 },
                            { "title": "24", "events": 48, "hours": 21 },
                            { "title": "25", "events": 89, "hours": 61 },
                            { "title": "26", "events": 77, "hours": 44 }
                          ]
                        },
                        {
                          "name": "Kitchen",
                          "color": "orange",
                          "data": [
                            { "title": "01", "events": 94, "hours": 59 },
                            { "title": "02", "events": 62, "hours": 48 },
                            { "title": "03", "events": 76, "hours": 38 },
                            { "title": "04", "events": 81, "hours": 62 },
                            { "title": "05", "events": 64, "hours": 27 },
                            { "title": "06", "events": 53, "hours": 31 },
                            { "title": "07", "events": 92, "hours": 65 },
                            { "title": "08", "events": 85, "hours": 42 },
                            { "title": "09", "events": 74, "hours": 35 },
                            { "title": "10", "events": 67, "hours": 55 },
                            { "title": "11", "events": 49, "hours": 23 },
                            { "title": "12", "events": 88, "hours": 75 },
                            { "title": "13", "events": 93, "hours": 66 },
                            { "title": "14", "events": 76, "hours": 34 },
                            { "title": "15", "events": 59, "hours": 39 },
                            { "title": "16", "events": 72, "hours": 51 },
                            { "title": "17", "events": 83, "hours": 44 },
                            { "title": "18", "events": 74, "hours": 33 },
                            { "title": "19", "events": 69, "hours": 28 },
                            { "title": "20", "events": 85, "hours": 56 },
                            { "title": "21", "events": 53, "hours": 22 },
                            { "title": "22", "events": 92, "hours": 70 },
                            { "title": "23", "events": 71, "hours": 41 },
                            { "title": "24", "events": 67, "hours": 25 },
                            { "title": "25", "events": 86, "hours": 74 },
                            { "title": "26", "events": 94, "hours": 68 }
                          ]
                        },
                        {
                          "name": "Other",
                          "color": "hotpink",
                          "data": [
                            { "title": "01", "events": 57, "hours": 27 },
                            { "title": "02", "events": 74, "hours": 33 },
                            { "title": "03", "events": 84, "hours": 53 },
                            { "title": "04", "events": 95, "hours": 68 },
                            { "title": "05", "events": 71, "hours": 48 },
                            { "title": "06", "events": 92, "hours": 76 },
                            { "title": "07", "events": 85, "hours": 62 },
                            { "title": "08", "events": 49, "hours": 25 },
                            { "title": "09", "events": 66, "hours": 38 },
                            { "title": "10", "events": 63, "hours": 31 },
                            { "title": "11", "events": 75, "hours": 47 },
                            { "title": "12", "events": 94, "hours": 72 },
                            { "title": "13", "events": 79, "hours": 49 },
                            { "title": "14", "events": 72, "hours": 45 },
                            { "title": "15", "events": 88, "hours": 61 },
                            { "title": "16", "events": 83, "hours": 52 },
                            { "title": "17", "events": 92, "hours": 76 },
                            { "title": "18", "events": 73, "hours": 40 },
                            { "title": "19", "events": 65, "hours": 28 },
                            { "title": "20", "events": 76, "hours": 63 },
                            { "title": "21", "events": 58, "hours": 30 },
                            { "title": "22", "events": 84, "hours": 67 },
                            { "title": "23", "events": 72, "hours": 41 },
                            { "title": "24", "events": 79, "hours": 46 },
                            { "title": "25", "events": 63, "hours": 29 },
                            { "title": "26", "events": 68, "hours": 39 }
                          ]
                        }
                      ]
                    },
                    {
                      "name": "6 Months",
                      "rooms": [
                        {
                          "name": "Bathroom",
                          "color": "purple",
                          "data": [
                            { "title": "October", "events": 62, "hours": 23 },
                            { "title": "November", "events": 76, "hours": 42 },
                            { "title": "December", "events": 85, "hours": 54 },
                            { "title": "January", "events": 94, "hours": 67 },
                            { "title": "February", "events": 63, "hours": 35 },
                            { "title": "March", "events": 81, "hours": 46 }
                          ]
                        },
                        {
                          "name": "Bedroom",
                          "color": "#3b82f6",
                          "data": [
                            { "title": "October", "events": 64, "hours": 35 },
                            { "title": "November", "events": 88, "hours": 71 },
                            { "title": "December", "events": 79, "hours": 54 },
                            { "title": "January", "events": 72, "hours": 49 },
                            { "title": "February", "events": 53, "hours": 32 },
                            { "title": "March", "events": 93, "hours": 67 }
                          ]
                        },
                        {
                          "name": "Kitchen",
                          "color": "orange",
                          "data": [
                            { "title": "October", "events": 92, "hours": 65 },
                            { "title": "November", "events": 85, "hours": 62 },
                            { "title": "December", "events": 74, "hours": 49 },
                            { "title": "January", "events": 63, "hours": 33 },
                            { "title": "February", "events": 78, "hours": 56 },
                            { "title": "March", "events": 69, "hours": 41 }
                          ]
                        },
                        {
                          "name": "Other",
                          "color": "hotpink",
                          "data": [
                            { "title": "October", "events": 88, "hours": 54 },
                            { "title": "November", "events": 72, "hours": 39 },
                            { "title": "December", "events": 84, "hours": 63 },
                            { "title": "January", "events": 76, "hours": 46 },
                            { "title": "February", "events": 93, "hours": 72 },
                            { "title": "March", "events": 68, "hours": 29 }
                          ]
                        }
                      ]
                    }
                  ]
                }

                payload = result_dictionary #{'result_dictionary': result_dictionary}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200
                AddToLog(payload)
                return


            elif function == "dashboard_list":
                # works in UTC only
                caretaker = user_name
                #date_s = form_data.get('date')
                time_s = form_data.get('time')
                date_s =  datetime.datetime.utcnow().strftime("%Y-%m-%d")
                filterr = form_data.get('filter')
                if filterr == None:
                    filterr = 5

                privileges = GetPriviledgesOnly(caretaker)

                deployments_list = GetUsersFromDeployments(privileges)

                #all_beneficiaries = ListBeneficiariesOfCaretaker(caretaker)      #GetPriviledgesOnly
                #AddToLog(all_beneficiaries)

                result_list = []

                for deployment_id, first_name, last_name in deployments_list:
                    details = GetSensorsDetailsFromDeployment(deployment_id, date_s, filterr)
                    if  details != {}:

                        details["units"] = "°C"
                        if "America" in details["time_zone"]:
                            details["temperature"] = CelsiusToFahrenheit(details["temperature"])
                            details["units"] = "°F"
                        devices_list, device_ids = GetProximityList(deployment_id, date_s)
                        # convert dates back to UTC
                        #details['bathroom_at']  = pytz.timezone(details['time_zone']).localize(datetime.datetime.strptime(details['bathroom_at'], "%Y-%m-%dT%H:%M:%S")).astimezone(pytz.UTC).strftime("%Y-%m-%dT%H:%M:%S")
                        #details['kitchen_at']   = pytz.timezone(details['time_zone']).localize(datetime.datetime.strptime(details['bathroom_at'], "%Y-%m-%dT%H:%M:%S")).astimezone(pytz.UTC).strftime("%Y-%m-%dT%H:%M:%S")
                        #details['bedroom_at']   = pytz.timezone(details['time_zone']).localize(datetime.datetime.strptime(details['bedroom_at'], "%Y-%m-%dT%H:%M:%S")).astimezone(pytz.UTC).strftime("%Y-%m-%dT%H:%M:%S")
                        #details['last_detected_time']   = pytz.timezone(details['time_zone']).localize(datetime.datetime.strptime(details['last_detected_time'], "%Y-%m-%dT%H:%M:%S")).astimezone(pytz.UTC).strftime("%Y-%m-%dT%H:%M:%S")
                        location_list = []
                        for room_details in devices_list:
                            well_id, device_id, location_name, description, MAC, radar_threshold_group_st, close_to = room_details #(266, 559, 'Bathroom', None, '64B70888FAB0', '["s3_max",12]')
                            location_list.append(location_name)

                        details["deployment_id"] = deployment_id
                        details["location_list"] = location_list
                        result_list.append(details)

                payload = {'result_list': result_list}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200
                AddToLog(payload)
                return


            elif function == "dashboard_single":
                caretaker = user_name
                #date_s = form_data.get('date')
                date_s =  datetime.datetime.utcnow().strftime("%Y-%m-%d")
                deployment_id = form_data.get('deployment_id')
                filterr = form_data.get('filter')
                if filterr == None:
                    filterr = 5


                #all_beneficiaries = ListBeneficiariesOfCaretaker(caretaker)      #GetPriviledgesOnly
                #AddToLog(all_beneficiaries)

                result_list = []

                details = GetSensorsDetailsFromDeployment(deployment_id, date_s, filterr)
                details["units"] = "°C"
                if "America" in details["time_zone"]:
                    details["temperature"] = CelsiusToFahrenheit(details["temperature"])
                    details["units"] = "°F"
                devices_list, device_ids = GetProximityList(deployment_id, date_s)
                location_list = []
                for room_details in devices_list:
                    well_id, device_id, location_name, description, MAC, radar_threshold_group_st, close_to = room_details #(266, 559, 'Bathroom', None, '64B70888FAB0', '["s3_max",12]')
                    location_list.append(location_name)

                details["deployment_id"] = deployment_id
                details["location_list"] = location_list
                settings =  {"wellness_score": False, "last_seen": False, "sleep_report": False, "activity_report": False, "temperature": True, "humidity": True, "air_pressure": True, "light": True, "air_quality": True, "radar": True, "other_activities": False}
                details["settings"] = settings


                result_list.append(details)

                payload = {'result_list': result_list}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200
                AddToLog(payload)
                return

            elif function == "request_node_red":
                logger.error(f"------------------------------- {function} ------------------------------------------")
                #this will:
                # 1.prepare folder and settings.js
                # 2.start instance on node-red and return it's return port
                #caretaker = user_name
                #date_s = form_data.get('date')
                time_s =  datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
                #deployment_id = form_data.get('deployment_id')
                #redis_conn.set('node_red_requests', str([radar_threshold_signal, radar_threshold_value]))
                # Hashes (dictionaries)
                logger.error(f"Storing to node_red_requests {user_name}")
                redis_conn.hset('node_red_requests', mapping={
                    'user_name': user_name,
                    'token': token,
                    'time': time_s,
                    'requests': 1
                })

                payload = {'ok': 1}
                logger.error(f"Responding {payload}")
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200

            elif function == "get_node_red_port":
                #this will:
                # 1.prepare folder and settings.js
                # 2.start instance on node-red and return it's return port
                hash_data = GetRedisMap(f'node_red_status_{user_name}')
                port = 0
                if hash_data != {}:
                    port = hash_data['port']
                #date_s = form_data.get('date')
                #date_s =  datetime.datetime.utcnow().strftime("%Y-%m-%d")
                #deployment_id = form_data.get('deployment_id')
                payload = {'port': port}
                logger.debug(f"get_node_red_port: {payload}")
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200
            elif function == "activity_detected":
                #this will:
                # 1.store to REDIS time of last activity
                time_s = form_data.get('time')

                hash_data = GetRedisMap(f'node_red_status_{user_name}')
                port = 0
                if hash_data != {}:
                    port = hash_data['port']

                redis_conn.hset(f'node_red_status_{user_name}', mapping={
                  'port': port,
                  'last_activity': time_s
                })

                payload = {'ok': 1}
                logger.debug(f"activity_detected: {payload}")
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200

            elif function == "store_flow":
                #this will:
                # 1.store flow into DB
                time_s = form_data.get('time')
                flow_json = form_data.get('flow')
                logger.debug(f"store_flow: {flow_json}")
                StoreFlow2DB(user_name, time_s, flow_json)
                payload = {'ok': 1}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200
            elif function == "node-red_deployed":
                #this will:
                # 1.store flow into DB
                time_s = form_data.get('time')
                logger.debug(f"node-red_deployed: {user_name}")
                redis_conn.hset('node-red_deployed', mapping={
                    'user_name': user_name,
                    'token': token,
                    'time': time_s,
                    'requests': 1
                })

                payload = {'ok': 1}
                logger.error(f"Responding {payload}")
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200

            elif function == "get_z_graph":
                #this will:
                # 1.store flow into DB
                deployment_id = form_data.get('deployment_id')
                list_of_devices = []
                if privileges == "-1" or deployment_id in privileges:
                    devices = GetVisibleDevices(deployment_id)


                if deployment_id != "":
                    #lets get all devices in this deployment
                    if privileges == "-1" or deployment_id in privileges:
                        devices = GetVisibleDevices(deployment_id)
                    if deployment_id in privileges:
                        devices = GetVisibleDevices(privileges)
                else:
                    pass
                #if all present, priorities are device_id, mac, well_id only one is sufficient
                device_id = form_data.get('device_id')
                well_id = form_data.get('well_id')
                mac = form_data.get('mac')

                #lets find device_id from any
                #if device_id != ""

                date_from = form_data.get('date_from')
                date_to = form_data.get('date_to')

                g_type = form_data.get('g_type')
                recreate = form_data.get('recreate')
                filter = form_data.get('filter')

                logger.debug(f"filter: {filter}")

                #StoreFlow2DB(user_name, time_s, flow_json)
                payload = {'ok': 1}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200

            else:
                AddToLog("Error: function not recognized!")
                payload = {'ok': 0, 'error': debug_string}
                resp.media = package_response(payload)
                resp.status = falcon.HTTP_200
                return

        except Exception as e:
            print(traceback.format_exc())
            resp.media = package_response(f"Error: {str(e)}  {traceback.format_exc()}", HTTP_500)


    def on_put(self, req, resp, path=""):
        """Handle PUT requests"""
        if path == "users":
            logger.info("PUT request to users endpoint")
            try:
                # Parse the request body
                request_data = json.loads(req.stream.read().decode('utf-8'))

                # TODO: Implement user update logic

                resp.status = HTTP_200
                resp.content_type = falcon.MEDIA_JSON
                resp.text = json.dumps({"id": request_data.get("id"), "message": "User updated"})
            except json.JSONDecodeError:
                resp.status = HTTP_400
                resp.content_type = falcon.MEDIA_JSON
                resp.text = json.dumps({"error": "Invalid JSON"})
        else:
            resp.media = package_response(f"PUT to /{path} not implemented", HTTP_400)

    def on_delete(self, req, resp, path=""):
        """Handle DELETE requests"""
        if path == "users":
            logger.info("DELETE request to users endpoint")
            resp.status = HTTP_200
            resp.content_type = falcon.MEDIA_JSON
            resp.text = json.dumps({"message": "User deleted"})
        else:
            resp.media = package_response(f"DELETE to /{path} not implemented", HTTP_400)

# Initialize data files


logger.error(f"------------------------------- STARTED ------------------------------------------")
try:
    searches_text = read_file("searches.json")
    searches_dict = json.loads(searches_text) if searches_text else {}

    dialogs_data = read_file("dialog.json")
    dialog_dict = json.loads(dialogs_data) if dialogs_data else {"utterances": {}, "intents": {}}

    intent_map = dialog_dict.get("utterances", {})
    utterances = {}
    for key in intent_map:
        logger.debug(key)
        list_of_utterances = intent_map[key]
        for utterance in list_of_utterances:
            utterances[utterance] = key

    intents = dialog_dict.get("intents", {})
except Exception as e:
    logger.error(f"Error initializing data files: {str(e)}")
    searches_dict = {}
    utterances = {}
    intents = {}

# Create Falcon application with middleware
middlewares = [CORSMiddleware(), RequestParser(), StripPathMiddleware()]
try:
    # For newer Falcon versions
    app = falcon.App(middleware=middlewares)
except:
    # For older Falcon versions
    app = falcon.API(middleware=middlewares)

#logger.error(f"@1")
# Add routes for well-api
well_api_instance = WellApi()

# New routes for well_api with multiple access paths
app.add_route('/function/well-api', well_api_instance)
app.add_route('/function/well-api/{path}', well_api_instance)
app.add_route('/api/well_api', well_api_instance)
app.add_route('/api/well_api/{path}', well_api_instance)
app.add_route('/healthz', well_api_instance, suffix='healthz')

# Add routes for the standard API paths
app.add_route('/health', well_api_instance)
app.add_route('/users', well_api_instance)
app.add_route('/items', well_api_instance)

# Keep the original routes for backward compatibility
app.add_route('/', well_api_instance)
app.add_route('/{path}', well_api_instance)

MQTTSERVERL = "eluxnetworks.net"
MQTT_PortL = 443

MyName = "well-api"

clientL = mqtt.Client(client_id=MyName+str(time.time()), transport="websockets")
clientL.tls_set(cert_reqs=ssl.CERT_NONE)  # For self-signed certs, use proper CA in production
clientL.ws_set_options(path="/mqtt")  # Important! Same path as in your JS code
clientL.username_pw_set("well_user","We3l1_best!")

clientL.on_connect = on_connectL
clientL.on_message = on_messageL

#clientL.connect(MQTTSERVERL, MQTT_PortL, 60)
#lientL.loop_start()
#logger.error(f"@2")

# This code runs when executed directly (for development/debugging)
if __name__ == "__main__":
    from wsgiref.simple_server import make_server
    redis_conn = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB)
    # Use port 8000 for local debugging
    port = int(os.environ.get('PORT', 8004))

    # Create a WSGI server
    with make_server('', port, app) as httpd:
        print(f'Serving on port {port}...')

        # Serve until process is killed
        httpd.serve_forever()
else:
    redis_conn = redis.Redis(host=redis_host, port=6379, db=0)
