import network
import time
import urequests
import ntptime
from machine import RTC, Pin
from neopixel import NeoPixel
from random import randint
""" Конфигурации """
# Настройки времени ---------------------------------------------------------------------
""" Часовой поясе UTC """
DEFAULT_TIMEZONE = 5 * (60 * 60)
""" Координаты, используются для определения TimeZone и получения погоды от Яндекса"""
LAT = 43.207508
LON = 76.883356
""" Timezone api """
TIMEZONE_URL = f'https://timeapi.io/api/TimeZone/coordinate?latitude={LAT}&longitude={LON}'
""" Интервал обновления секундного светодиода на дисплее"""
BLINKING_SECOND_INTERVAL = 1000 # ms
""" Интервал для синхронизации времени с сервером. Предварительно раз в сутки"""
# (60 * 60 * 1000) - Получения часов из миллисекунд
TIME_SYNCHRONIZE_INTERVAL = 24 * (60 * 60 * 1000) # ms
# Адресные светодиоды -------------------------------------------------------------------
""" Количество подключенных светодиодов """
LED_COUNT = 79
""" Номер пина, к которому подключены светодиоды """
PIN = 14
""" Цвет дисплея по умолчанию """
DEFAULT_COLOR = (255, 20, 147)
# Параметры Wi-Fi сети ------------------------------------------------------------------
""" Название точки wifi """
WIFI_SSID = "Wokwi-GUEST"
""" Пароль от wifi """
WIFI_PASSWORD = ""
# Яндекс погода -------------------------------------------------------------------------
""" Endpoint """
# TODO: Mock, не забыть заменить
# Плюсовая https://run.mocky.io/v3/3b0845f1-ad49-45bf-8120-6feacdd2ebca
# Минусовая https://run.mocky.io/v3/53445461-c6e6-4f1f-af09-33b867b8770d
# 'https://api.weather.yandex.ru/v2/informers'
API_WEATHER_YANDEX = 'https://run.mocky.io/v3/3b0845f1-ad49-45bf-8120-6feacdd2ebca'
""" Токен авторизации """
X_YANDEX_API_KEY = 'token'
""" Интервал обновления погоды """
# Яндекс на бесплатном тарифе даёт 50 запросов на сутки, можно обновлять погоду каждые 30 мин
# (60 * 60 * 1000) - Получения часов из миллисекунд
YANDEX_UPDATE_INTERVAL = 0.5 * (60 * 60 * 1000) # ms
# ---------------------------------------------------------------------------------------
class Wifi:
""" Класс для взаимодействия с wi-fi """
@staticmethod
def connect() -> None:
""" Подключение к Wi-Fi
:return: None
"""
sta_if = network.WLAN(network.STA_IF)
# TODO: костылик для мерцания светодиода при подключении wifi
np = NeoPixel(Pin(PIN), LED_COUNT)
led_start_id = 0
if not sta_if.isconnected():
print("Connecting to Wi-Fi...")
sta_if.active(True)
sta_if.connect(WIFI_SSID, WIFI_PASSWORD)
while not sta_if.isconnected():
np[led_start_id] = (randint(0, 255), randint(0, 255), randint(0, 255))
np.write()
led_start_id +=1
time.sleep(0.2)
if led_start_id == LED_COUNT-1:
led_start_id = 0
for i in range(LED_COUNT):
np[i] = (0,0,0)
np.write()
print("Wi-Fi connected!")
class Time:
""" Класс для работы со временем """
def __init__(self):
self.utc_timezone = self.get_timezone_in_seconds()
self.sync_time()
def sync_time(self) -> None:
""" Синхронизация времени с сервером NTP с учетом часового пояса
:return: None
"""
print('sync_time')
ntptime.settime() # Получить текущее время с сервера NTP
time_now = time.localtime(time.time() + self.utc_timezone) # Подсчёт времени с учётом часового пояса
RTC().datetime((time_now[0], time_now[1], time_now[2], time_now[6], time_now[3], time_now[4], time_now[5], 0))
@staticmethod
def format_time() -> list:
""" Функция для форматирования времени.
Если время 1:9, для корректного отображения на дисплее добавляется нули, результат 01:09
:return: list
"""
time_now = RTC().datetime()
hour = str(time_now[4])
hour = f'0{hour}' if len(hour) == 1 else hour
minute = str(time_now[5])
minute = f'0{minute}' if len(minute) == 1 else minute
print(f'format_time. Now: {list(hour + minute)}')
return list(f'{hour}{minute}')
@staticmethod
def get_timezone_in_seconds() -> int:
""" Получение UTC timezone в секундах
:return:
"""
timezone_second = Requests().send_get_request(url = TIMEZONE_URL)['currentUtcOffset']['seconds']
return int(timezone_second) if timezone_second is None else DEFAULT_TIMEZONE
class YandexWeather:
""" Класс взаимодействия с Янедкс погодой """
def __init__(self):
self.data_yandex_weather = self.get_data_yandex_weather()
self.temp = self.get_temp()
self.condition = self.get_condition()
def get_temp(self) -> list:
""" Получение температуры погоды
:return: list
"""
print('get_temp')
temp_now = str(self.data_yandex_weather['fact']['temp'])
temp_satus_now = '-' if int(temp_now) <= 0 else '+' # TODO: Магия. Подстановки состояния погоды
temp_now = temp_now.replace('-', '')
temp_now = f'0{temp_now}' if len(temp_now) == 1 else temp_now # TODO: Магическое подстановка 0
return list(temp_satus_now + temp_now)
def get_condition(self) -> str:
""" Получение состояния погоды(Солнечно, дождливо, облачно и т.д.)
:return: str
"""
print('get_condition')
return self.data_yandex_weather['fact']['condition']
@staticmethod
def get_data_yandex_weather():
""" Получение данных о погоде с Яндекс апи
:return:
"""
print('get_data_yandex_weather')
return Requests().send_get_request(
url = API_WEATHER_YANDEX,
headers = {'X-Yandex-API-Key': X_YANDEX_API_KEY}
)
class Requests:
""" Класс для работы с запросами """
@staticmethod
def send_get_request(url, headers = None):
""" Функция для отправки GET запроса и вывода ответа
:return:
"""
try:
# TODO: Говнокод
if headers is None:
response = urequests.get(url)
else:
response = urequests.get(url, headers = headers)
if response.status_code == 200:
return response.json()
else:
print("Error: HTTP status code", response.status_code)
response.close()
except Exception as e:
print("Error:", e)
return None
class ShowOnDisplay:
""" Класс для взаимодействия с адресными светодиодами """
def __init__(self):
self.np = NeoPixel(Pin(PIN), LED_COUNT)
self.started_display()
self.yandex_weather_base = YandexWeather()
self.time_now_base = Time()
self.time_now = self.time_now_base.format_time()
self.temperature = self.yandex_weather_base.temp
self.conditional = self.yandex_weather_base.condition
self.color_display = ColorDisplay().set_color(self.temperature)
# Дисплей состояния погоды
# TODO: Магия
WEATHER_CONDITIONAL_DISPLAY = {
'clear': 62, # ясно.
'partly-cloudy': 63, # малооблачно.
'cloudy': 64, # облачно с прояснениями.
'overcast': 65, # пасмурно.
'light-rain': 66, # небольшой дождь.
'rain': 67, # дождь.
'heavy-rain': 68, # сильный дождь.
'showers': 69, # ливень.
'wet-snow': 70, # дождь со снегом.
'light-snow': 71, # небольшой снег.
'snow': 72, # снег.
'snow-showers': 73, # снегопад.
'hail': 74, # град.
'thunderstorm': 75, # гроза.
'thunderstorm-with-rain': 76, # дождь с грозой.
'thunderstorm-with-hail': 77, # гроза с градом.
}
def show_time(self) -> None:
""" Функция для отображения времени
:return:
"""
time_now = self.time_now_base.format_time()
self.off_display('time')
time_display_index = 3 # TODO: Магическое число
for i in time_now:
self.np[int(f'{time_display_index}{i}')] = self.color_display
time_display_index -= 1
self.np.write()
def show_temperature(self) -> None:
""" Функция для отображения температуры
:return: None
"""
self.off_display('temperature')
temperature_display_index = 6 # TODO: Магическое число
# TODO: Костыль
for i in self.temperature:
if i == '+':
self.np[60] = self.color_display
elif i == '-':
self.np[61] = self.color_display
else:
self.np[int(f'{temperature_display_index}{i}')] = self.color_display
temperature_display_index -= 1
self.np.write()
def show_weather_conditional(self) -> None:
""" Функция для отображения состояния погоды
:return: None
"""
self.off_display('conditional')
self.np[self.WEATHER_CONDITIONAL_DISPLAY[self.conditional]] = self.color_display
self.np.write()
def off_display(self, display) -> None:
""" Функция выключения светодиодов
:return: None
"""
# TODO: Полно магии
if display == "time":
for i in range(0, 39):
self.np[i] = (0, 0, 0)
elif display == "temperature":
for i in range(40, 61):
self.np[i] = (0, 0, 0)
elif display == "conditional":
for i in range(
self.WEATHER_CONDITIONAL_DISPLAY['clear'],
self.WEATHER_CONDITIONAL_DISPLAY['thunderstorm-with-hail']
):
self.np[i] = (0, 0, 0)
elif display == "second":
self.np[LED_COUNT - 1] = (0, 0, 0) # TODO: костыль
else:
for i in range(LED_COUNT):
self.np[i] = (0, 0, 0)
self.np.write()
def show_blinking_second(self) -> None:
""" Мигающий индикатор секунды
:return: None
"""
self.np[LED_COUNT - 1] = self.color_display # TODO: костыль
self.np.write()
def started_display(self) -> None:
""" Включение всех светодиодов при включении часов
:return: None
"""
print('started_display')
for i in range(LED_COUNT):
self.np[i] = (randint(0, 255), randint(0, 255), randint(0, 255))
self.np.write()
time.sleep(0.08)
self.np[i] = (0, 0, 0)
self.np.write()
class ColorDisplay:
""" Класс для работы с цветом """
def __init__(self):
self.color = DEFAULT_COLOR
def set_color(self, temp) -> tuple:
""" Выбор цвета дисплея в зависимости от текущей температуры
:param temp: Текущая температура
:return: tuple
"""
print('set_color')
temp = int(''.join(temp))
# TODO: Тяжёлый для восприятия elif
if temp <= -20:
color = (0, 206, 209)
elif -19 <= temp <= -10:
color = (127, 255, 212)
elif -9 <= temp <= -1:
color = (0, 255, 255)
elif 0 <= temp <= 5:
color = (135, 206, 235)
elif 6 <= temp <= 10:
color = (152, 251, 152)
elif 11 <= temp <= 15:
color = (124, 252, 0)
elif 16 <= temp <= 20:
color = (0, 255, 0)
elif 21 <= temp <= 25:
color = (255, 215, 0)
elif 25 <= temp:
color = (220, 20, 60)
else:
color = self.color
return color
Wifi().connect()
show_display_base = ShowOnDisplay()
def setup():
""" Отображение данных при запуске
:return:
"""
show_display_base.show_time()
show_display_base.show_temperature()
show_display_base.show_weather_conditional()
setup()
# Интервалы старта, для запуска функций через интервал
intervals_start = {
'yandex': time.ticks_ms(), # Интервал старта обновления Яндекс погоды
'synchronize_time': time.ticks_ms(), # Интервал старта обновления синхронизации времени
'show_time': RTC().datetime()
}
blinking_second_data = {
'blinking_second': time.ticks_ms(),
'is_on': True
}
while True:
if time.ticks_diff(time.ticks_ms(), intervals_start['yandex']) >= YANDEX_UPDATE_INTERVAL:
show_display_base.yandex_weather_base.get_data_yandex_weather()
show_display_base.show_temperature()
show_display_base.show_weather_conditional()
intervals_start['yandex'] = time.ticks_ms()
if time.ticks_diff(time.ticks_ms(), intervals_start['synchronize_time']) >= TIME_SYNCHRONIZE_INTERVAL:
# Синхронизация времени с UTC сервером через интервал
show_display_base.time_now_base.sync_time()
intervals_start['synchronize'] = time.ticks_ms()
if time.ticks_diff(time.ticks_ms(), blinking_second_data['blinking_second']) >= BLINKING_SECOND_INTERVAL:
# Отображение мигающего секундного светодиода
if blinking_second_data['is_on']:
show_display_base.show_blinking_second()
blinking_second_data['is_on'] = False
blinking_second_data['blinking_second'] = time.ticks_ms()
else:
show_display_base.off_display('second')
blinking_second_data['is_on'] = True
blinking_second_data['blinking_second'] = time.ticks_ms()
if RTC().datetime()[5] > intervals_start['show_time'][5]: # TODO: магические цифры
# Отображение времени на дисплее
show_display_base.show_time()
intervals_start['show_time'] = RTC().datetime()
print(RTC().datetime())
time.sleep(1)