CCCPaste
# SPDX-FileCopyrightText: 2019 Dave Astels for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""
PyPortal based alarm clock.

Adafruit invests time and resources providing this open source code.
Please support Adafruit and open source hardware by purchasing
products from Adafruit!

Written by Dave Astels for Adafruit Industries
Copyright (c) 2019 Adafruit Industries
Licensed under the MIT license.

All text above must be included in any redistribution.
"""

# pylint:disable=redefined-outer-name,no-member,global-statement, line-too-long, invalid-name
# pylint:disable=no-self-use,too-many-branches,too-many-statements, import-error
# pylint:disable=useless-super-delegation, too-many-locals, no-name-in-module

import math
import time
import board
from adafruit_bitmap_font import bitmap_font # type: ignore
from adafruit_display_text.label import Label # pyright: ignore[reportMissingImports]
import adafruit_sgp40 # pyright: ignore[reportMissingImports]
import analogio
# import displayio
import adafruit_logging as logging # pyright: ignore[reportMissingImports]
import bitbangio
import rtc
import adafruit_gps # pyright: ignore[reportMissingImports]
import neopixel # pyright: ignore[reportMissingImports]
from microcontroller import nvm
from adafruit_bme280 import basic as bmE280 # pyright: ignore[reportMissingImports]TZ_CUR = nvm[0]  # type: ignore
import adafruit_sht31d
import adafruit_lps2x
from adafruit_pyportal import PyPortal # type: ignore

# read time zone
if nvm[0] > 23 or nvm[0] < 0: # pyright: ignore[reportOptionalSubscript]
    TZ_CUR = 4
    nvm[0] = 4 # pyright: ignore[reportOptionalSubscript]
else:
    TZ_CUR = nvm[0]  # type: ignore
####################
# setup hardware
# i2c = board.I2C()
# i2c = busio.I2C(board.SCL, board.SDA, frequency=100_000)

# bitbangio used due to its implementation working better (less i2c errors)
i2c = bitbangio.I2C(board.SCL, board.SDA, frequency=100_000)  # type: ignore
gps = adafruit_gps.GPS_GtopI2C(i2c, debug=False)  # type: ignore # Use I2C interface

gps.send_command(b"PMTK314,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0")
gps.send_command(b"PMTK220,1000")

pixels = neopixel.NeoPixel(board.NEOPIXEL, 1)  # type: ignore
bmE280.sea_level_pressure = 1013.25  # type: ignore
outside = adafruit_sht31d.SHT31D(i2c)
inside = bmE280.Adafruit_BME280_I2C(i2c)  # type: ignore
lps = adafruit_lps2x.LPS22(i2c)
sgp = adafruit_sgp40.SGP40(i2c)

while gps.timestamp_utc is None:
    print("No time data from GPS yet")
    pixels[0] = (0, 0, 0)
    time.sleep(.7)
    pixels[0] = (128, 0, 0)
    time.sleep(.7)
    gps.update()
    print(gps.readline())
pixels[0] = (0, 0, 0)
the_rtc = rtc.RTC()
the_rtc.datetime = time.struct_time(gps.timestamp_utc)


pyportal = PyPortal()

light = analogio.AnalogIn(board.LIGHT)  # type: ignore

# init logfile
# t_stamp = "{0:x}".format(time.time())
DELIMETER = "\t"
LOG_FILE = f"/sd/{time.time()}gps.txt"
LOG_MODE = "a"
with open(LOG_FILE, LOG_MODE, encoding="utf-8") as outfile:  # type: ignore
    print("UTC",DELIMETER,"lat",DELIMETER,"long",DELIMETER,"alt",DELIMETER,"Speed",DELIMETER,"intemp",DELIMETER,"intemp2",DELIMETER,"inh",DELIMETER,"otemp",DELIMETER,"outh",DELIMETER,"barr1",DELIMETER,"barr2",DELIMETER,"airQ", file=outfile)

print("envlogfile created")
####################
# variables
Speed = 0
# alarm support
"""
alarm_background = 'red_alertl.bmp'
alarm_file = 'alarm.wav'
alarm_enabled = False
alarm_armed = False
alarm_interval = 10.0
alarm_hour = 9
alarm_minute = 45
snooze_time = None
snooze_interval = 600.0
"""
# pressure support
# mugsy_background = 'mugsy_backgroundl.bmp'

# weather support

icon_file = None
icon_sprite = None
celcius = None


# display/data refresh timers

refresh_time = None
update_time = None
weather_refresh = None

# The most recently fetched time
current_time = None

# track whether we're in low light mode

low_light = False


####################
# Load the fonts
print("loading fonts")
time_font = bitmap_font.load_font('/fonts/Asimov-104.pcf')
time_font.load_glyphs(b'0123456789:%')  # pre-load glyphs for fast printing  # type: ignore

alarm_font = bitmap_font.load_font('/fonts/AsimovNar-45.pcf')
alarm_font.load_glyphs(b'0123456789mph.:%')  # type: ignore

title_font = bitmap_font.load_font('/fonts/Asimov-25.pcf')
title_font.load_glyphs(b'INSIDEOUT')  # type: ignore

temperature_font = bitmap_font.load_font('/fonts/AsimovNar-40.pcf')
temperature_font.load_glyphs(b'0123456789INSIDEOUTFft.%')  # type: ignore

print("fonts loaded")
####################
# Set up logging

logger = logging.getLogger('alarm_clock')
logger.addHandler(logging.FileHandler("/sd/syslogz.txt", "a"))
logger.setLevel(logging.INFO)            # change as desired  # type: ignore
print("logger loaded")
####################
# Functions
# def debug(*args, **kwargs):
    # lets both print and write to the file
    # print(*args, **kwargs)
    # print(*args, **kwargs, file=outfile)  # type: ignore

def create_text_areas(configs):
    """Given a list of area specifications, create and return text areas."""
    print("creating text areas")
    text_areas = []
    for cfg in configs:
        textarea = Label(cfg['font'], text=' '*cfg['size'])
        textarea.x = cfg['x']
        textarea.y = cfg['y']
        textarea.color = cfg['color']
        text_areas.append(textarea)
    return text_areas

def calc_dewpoint(humidity, temp_c):
    """Calculate dew point given relative humidity and temperature in Celsius."""
    a = 17.625
    b = 243.04
    alpha = math.log(humidity/100.0) + ((a * temp_c) / (b + temp_c))
    return (b * alpha) / (a - alpha)

def clear_splash():
    """clear the screen"""
    print("clear_splash")
    for _ in range(len(pyportal.root_group) - 1):
        pyportal.root_group.pop()


def touch_in_button(t, b):
    """touch"""
    in_horizontal = b['left'] <= t[0] <= b['right']
    in_vertical = b['top'] <= t[1] <= b['bottom']
    return in_horizontal and in_vertical


touched = False

####################
# states

class State(object):
    """State abstract base class"""

    def __init__(self):
        pass

    @property
    def name(self):
        """Return the name of teh state"""
        return ''

    def tick(self, now):
        """Handle a tick: one pass through the main loop"""

    # pylint:disable=unused-argument
    def touch(self, t, touched):

        """Handle a touch event.
        :param (x, y, z) - t: the touch location/strength"""
        return bool(t)

    def enter(self):
        """Just after the state is entered."""
        print(State)

    def exit(self):
        """Just before the state exits."""
        clear_splash()

class Time_State(State):
    """This state manages the primary time display screen/mode"""

    def __init__(self):
        super().__init__()
        self.background_day = 'main_background_dayl.bmp'
        self.background_night = 'main_background_nightl.bmp'
        self.refresh_time = None
        self.update_time = None
        self.weather_refresh = None
        text_area_configs = [dict(x=160, y=235, size=5, color=0xFFFFFF, font=time_font),  # TIME
                             dict(x=325, y=100, size=5, color=0xE09941, font=alarm_font),  # SPEED
                             dict(x=120, y=33, size=16, color=0xCFFC03, font=title_font),  # screen heading
                             dict(x=125, y=70, size=6, color=0xFFFFFF, font=temperature_font),  # INSIDE temp
                             dict(x=125, y=115, size=6, color=0xFFFFFF, font=temperature_font),  # INSIDE humid
                             dict(x=215, y=70, size=6, color=0xFFFFFF, font=temperature_font),  # OUTSIDE temp
                             dict(x=215, y=115, size=6, color=0xFFFFFF, font=temperature_font),  # OUTSIDE humid
                             dict(x=145, y=170, size=12, color=0x32A89E, font=temperature_font),  # Altitude
                             dict(x=310, y=180, size=5, color=0xCFFC03, font=title_font)]  # Air Quality
        self.text_areas = create_text_areas(text_area_configs)
        # self.weather_icon = displayio.Group()
        # self.weather_icon.x = 125
        # self.weather_icon.y = 25
        # self.icon_file = None
        print("creating buttons")

        # each button has it's edges as well as the state to transition to when touched
        self.buttons = [dict(left=0, top=72, right=118, bottom=158, next_state='settings'),
                        dict(left=0, top=208, right=118, bottom=294, next_state='pressurel')]

    @property
    def name(self):
        return 'time'

    def adjust_backlight_based_on_light(self, force=False):
        """Check light level. Adjust the backlight and background image if it's dark."""
        global low_light
        if light.value <= 800 and (force or not low_light):
            pyportal.set_backlight(0.01)
            pyportal.set_background(self.background_night)
            low_light = True
        elif force or (light.value >= 2000 and low_light):
            pyportal.set_backlight(1.00)
            pyportal.set_background(self.background_day)
            low_light = False

    def tick(self, now):
        global update_time, outfile

        # check light level and adjust background & backlight
        self.adjust_backlight_based_on_light()

        # only query the online time once per hour (and on first run)
        if (not self.refresh_time) or ((now - self.refresh_time) > 3600):
            logger.debug('Fetching time')
            try:
                gps.update()
                the_rtc.datetime = time.struct_time(gps.timestamp_utc)  # type: ignore
                self.refresh_time = now
            except RuntimeError as e:
                self.refresh_time = now - 3000   # delay 10 minutes before retrying
                logger.error('Some error occured, retrying! - %s', str(e))

        # only query the weather every 10 minutes (and on first run)
        # if (not self.weather_refresh) or (now - self.weather_refresh) > 1:
        print("reading sensors")
        logger.debug('Fetching weather')
        try:
            gps.update()
            if gps.speed_knots is not None:
                Speed = '%5d mph' % round(gps.speed_knots * 1.151)  # type: ignore
            else:
                Speed = "0"
            self.text_areas[1].text = Speed
            i1 = (inside.temperature * 9 / 5) + 32
            i2 = (lps.temperature * 9 / 5) + 32
            intemp = '%3d F' % round(i1)
            intemp2 = '%3d F' % round(i2)
            it_text = '%3d F' % round((i1 + i2) / 2)
            self.text_areas[2].text = "INSIDE  OUTSIDE"
            self.text_areas[3].text = it_text
            inrh = inside.relative_humidity
            humi_text = '%3d' % round((calc_dewpoint(inside.relative_humidity, inside.temperature)* 9 / 5) + 32 )
            self.text_areas[4].text = humi_text
            outtemp = outside.temperature
            ot_text = '%3d F' % round(((outtemp * 9 / 5) + 32))  # type: ignore
            self.text_areas[5].text = ot_text
            humo_text = '%3d' % round((calc_dewpoint(outside.relative_humidity, outside.temperature)* 9 / 5) + 32 )           # type: ignore
            print("humo_text:", (calc_dewpoint(outside.relative_humidity, outside.temperature)* 9 / 5) + 32)
            self.text_areas[6].text = humo_text
            VOCIdx = sgp.measure_index(temperature = inside.temperature, relative_humidity = inrh)
            self.text_areas[8].text = '%3d' % round(VOCIdx)
            if gps.altitude_m is not None:
                altftxt = '%6.2d ft' % round(gps.altitude_m * 3.28084, 2)
            else:
                altftxt = "0"
            self.text_areas[7].text = altftxt
            self.weather_refresh = now
            t_stamp = time.time()
            with open(LOG_FILE, LOG_MODE, encoding="utf-8") as outfile:
                print(t_stamp, DELIMETER, gps.latitude, DELIMETER, gps.longitude, DELIMETER, altftxt, DELIMETER, Speed,
                 DELIMETER, intemp, DELIMETER, intemp2, DELIMETER, humi_text, DELIMETER, ot_text, humo_text, " ", inside.pressure, DELIMETER, lps.pressure, " ", VOCIdx, file=outfile)
            try:
                board.DISPLAY.refresh(target_frames_per_second=60)  # type: ignore
            except AttributeError:
                board.DISPLAY.refresh_soon()  # type: ignore
                board.DISPLAY.wait_for_frame()  # type: ignore
        except RuntimeError as e:
            self.weather_refresh = now - 540   # delay a minute before retrying
            logger.error("Some error occured, retrying! - %s", str(e))

        if (not update_time) or ((now - update_time) > 30):
            # Update the time
            update_time = now
            current_time = time.localtime()
            time_string = '%02d:%02d' % (((current_time.tm_hour-TZ_CUR) % 24), current_time.tm_min)
            self.text_areas[0].text = time_string

            try:
                board.DISPLAY.refresh(target_frames_per_second=60)  # type: ignore
            except AttributeError:
                board.DISPLAY.refresh_soon()  # type: ignore
                board.DISPLAY.wait_for_frame()  # type: ignore

    def touch(self, t, touched):
        if t and not touched:             # only process the initial touch
            for button_index in enumerate(self.buttons):
                b = self.buttons[button_index]
                if touch_in_button(t, b):
                    change_to_state(b['next_state'])  # type: ignore
                    break
        return bool(t)

    def enter(self):
        self.adjust_backlight_based_on_light(force=True)
        for ta in self.text_areas:
            pyportal.root_group.append(ta)
        # pyportal.root_group.append(self.weather_icon)
        try:
            board.DISPLAY.refresh(target_frames_per_second=60)  # type: ignore
        except AttributeError:
            board.DISPLAY.refresh_soon()  # type: ignore
            board.DISPLAY.wait_for_frame()  # type: ignore

    def exit(self):
        super().exit()

class Pressure_State(Time_State):
    """This state records a pressure log """

    def __init__(self):
        super().__init__()
        text_area_configs = [dict(x=160, y=220, size=3, color=0xFFAAFF, font=time_font)]

        self.text_areas = create_text_areas(text_area_configs)

    @property
    def name(self):
        return 'pressurel'

    def tick(self, now):
        # Once the job is done, go back to the main screen
        change_to_state('time')  # type: ignore

    def enter(self):
        global low_light
        for ta in self.text_areas:
            pyportal.root_group.append(ta)

        low_light = False
        plog_path = "/sd/"
        # pyportal.set_backlight(1.00)
        # pyportal.set_background(mugsy_background)
        lps.data_rate = adafruit_lps2x.Rate.LPS22_RATE_75_HZ  # type: ignore
        gps.update()
        t_stamp = time.time()
        PLOG_FILE = f"{t_stamp}pressure.txt"
        start = time.monotonic_ns() / 1e9
        with open((plog_path+PLOG_FILE), LOG_MODE, encoding="utf-8") as pressfile:
            print("timestamp, latitude, longitude, altitude_m, \n", file=pressfile)
            print(t_stamp, gps.latitude, gps.longitude, gps.altitude_m, file=pressfile)
            curr_time = 0
            while curr_time < 60:
                curr_time = (time.monotonic_ns() / 1e9) - start
                print(curr_time, lps.pressure, file=pressfile)
                cntdwn = '%3d' % round((60 - curr_time))
                self.text_areas[0].text = cntdwn
                try:
                    board.DISPLAY.refresh(target_frames_per_second=60)  # type: ignore
                except AttributeError:
                    board.DISPLAY.refresh_soon()  # type: ignore
                    board.DISPLAY.wait_for_frame()  # type: ignore

        lps.data_rate = adafruit_lps2x.Rate.LPS22_RATE_1_HZ  # type: ignore

class Setting_State(State):
    """This state lets the user adjust UTC offset."""

    def __init__(self):
        super().__init__()
        self.previous_touch = None
        self.background = 'settings_backgroundl.bmp'
        text_area_configs = [dict(x=160, y=220, size=5, color=0xFFFFFF, font=time_font)]

        self.text_areas = create_text_areas(text_area_configs)
        self.buttons = [dict(left=0, top=40, right=115, bottom=123),    # increase
                        dict(left=0, top=130, right=115, bottom=150),   # return
                        dict(left=0, top=160, right=115, bottom=295)]  # decrease
                        # dict(left=150, top=0, right=300, bottom=320),  # hours
                        # dict(left=310, top=0, right=480, bottom=320)]   # minutes
        time_string = '%02d' % TZ_CUR
        self.text_areas[0].text = time_string

    @property
    def name(self):
        return 'settings'

    def touch(self, t, touched):
        global TZ_CUR
        if t:
            if touch_in_button(t, self.buttons[0]):   # increase offset
                logger.debug('increase offset')
                TZ_CUR += 1
                time_string = '%02d' % TZ_CUR
                self.text_areas[0].text = time_string
            elif touch_in_button(t, self.buttons[1]):   # return
                logger.debug('RETURN touched')
                nvm[0] = TZ_CUR  # type: ignore
                change_to_state('time')
            elif touch_in_button(t, self.buttons[2]):  # decrease offset
                logger.debug('decrease offset')
                TZ_CUR -= 1
                time_string = '%02d' % TZ_CUR
                self.text_areas[0].text = time_string
            try:
                board.DISPLAY.refresh(target_frames_per_second=60)  # type: ignore
            except AttributeError:
                board.DISPLAY.refresh_soon()  # type: ignore
                board.DISPLAY.wait_for_frame()  # type: ignore

        else:
            self.previous_touch = None
        return bool(t)

    def enter(self):

        pyportal.set_background(self.background)
        for ta in self.text_areas:
            pyportal.root_group.append(ta)
        # if alarm_enabled:
            # self.text_areas[0].text = '%02d:%02d' % (alarm_hour, alarm_minute)  # set time textarea
        # else:
            # self.text_areas[0].text = '     '


####################
# State management

states = {'time': Time_State(),
          'pressurel': Pressure_State(),
          'settings': Setting_State()}

current_state = None


def change_to_state(state_name):
    """."""
    global current_state
    if current_state:
        logger.debug('Exiting %s', current_state.name)
        current_state.exit()
    current_state = states[state_name]
    logger.debug('Entering %s', current_state.name)
    current_state.enter()

####################
# And... go

clear_splash()
change_to_state("time")

while True:
    touched = current_state.touch(pyportal.touchscreen.touch_point, touched)  # type: ignore
    current_state.tick(time.monotonic())  # type: ignore