Свет мой, зеркальце

О проекте Magic Mirror я узнал довольно давно. Некоторое время интерес был совсем абстрактным. Ну да, прикольно. Ну да, знаю, что могу сделать что-то такое же, если захочу. Но захочу ли? У меня на кухне уже долгое время и так живёт андроидный планшет, в котором при помощи самопального PWA показывается фотография из архива, время, календарь на текущий месяц с отмеченными выходными, праздниками и всякими годовщинами. И погода. Все данные кроме погоды подтягиваются из домашней сети. Погоду я всё это время брал из Dark Sky.

Небольшое отступление

В таких отступлениях под спойлером буду скрывать то что не относится напрямую к рассказываемой истории, но позволит лучше понять причины и детали тех или иных принимавшихся решений.

Небольшое отступление

В моей домашней сети есть сервер с тучей виртуалок под разное — начиная с DHCP-, DNS-, LDAP-серверов, HTTP, файл- и медиасервер, пара серверов баз данных и всякой прочей нечисти, без которой современному человеку трудно себе представить жизнь.

В какой-то момент для контроля за этим зоопарком была написана система велосипедов и костылей мониторинга: БД событий, агенты, триггеры на оповещение. В качестве транспортного протокола используется MQTT потому что почему бы нет.

По ненаучным, никак не обоснованным оценкам, прогнозы Dark Sky мне показались вполне адекватными, а текущая погода не слишком отличалась от того что творилось за окном. Ну и бесплатные лимиты API были вполне комфортными. А потом началось. Dark Sky была приобретена Apple. Apple заявила что приложение мы убьём вотпрямщас, а API ещё некоторое время пусть живёт. Наконец, дошла очередь и до API. Вот тут я и забеспокоился. Пошёл на Apple выяснять что такое этот их WeatherKit. Узнал много для себя нового. Например, ещё не начав что-либо делать, я уже им должен. $99 в год чтобы зарегистрироваться как разработчик и получить ключ.

Да в гробу я вас видел, подумал я. Что, вы последние, что ли, кто даёт погодное API? А пока я буриданил насчёт нового провайдера и собирался с силами, блок с погодой на кухонном планшете почернел без надежды на восстановление его работы.

Небольшое отступление

82fd7fd4e948a8ce31fe3086655b9b07.png

Когда я его делал, я подумал, что надо бы как-то сообщать о том, что в состоянии API что-то не так. Каждые 10 минут я делаю запрос API, при удачном запросе свойство opacity нужных объектов устанавливается в единицу. Каждый неудачный запрос уменьшает opacity. Примерно вот так:

function displayForecast(data)
{
    if (currentOpacity != 1)
    {
        currentOpacity = 1;
        $('#current-weather').css({ 'opacity': currentOpacity });
        $('#forecast').css({ 'opacity': currentOpacity });
        $('#forecast-chart').css({ 'opacity': currentOpacity });
    }
    ...
}

function noForecast()
{
    if (currentOpacity > 0.2)
    {
        currentOpacity -= 0.05;
        $('#current-weather').css({ 'opacity': currentOpacity });
        $('#forecast').css({ 'opacity': currentOpacity });
        $('#forecast-chart').css({ 'opacity': currentOpacity });
    }
}

(Да, у меня там вовсю jQuery. Я старый солдат и чищу ружья кирпичом).

Соответственно, зайдя на кухню, бросив взгляд на планшет и увидев чёрное пятно, можно понять, что нет интернета, либо планшет не видит сеть, либо отвалился API. Первые два типа проблем отлавливаются мониторингом, а вот для API планшет сам такой мониторинг.

И тут по весне произошли 2 события, которые реанимировали мой интерес к зеркалу. Во-первых, жена как-то задумчиво сказала «почему бы нам в коридоре не переклеить обои». А во-вторых, у одного из сотрудников конторы я увидел внешний монитор. 15», питание по Type C, видео по mini HDMI или Type C. В результате было принято решение сделать в коридоре полноценный ремонт, вишенкой на котором будет повешение в углу около входной двери умного зеркала.

Небольшое отступление

Дело в том, что у меня в углу коридора около входной двери вертикально проходит какая-то общедомовая труба. Эта труба была прикрыта листом фанеры, на котором мы в своё время повесили обычное зеркало.

Вот с чего всё начиналось

Вот с чего всё начиналось

Соответственно, как часть ремонта переделывается вся входная группа, в том числе — сносится входная дверь и весь входной проём. Ставится новая дверь, входной проём закладывается кирпичом, а не фанерой и паклей, как это было до того как мы переехали и в течение тех 20 лет что мы в этой квартире жили.

Посмотрел, как люди сейчас делают такие зеркала. Увиденное (например, вот это) мне не понравилось. Рама для зеркала глубиной сантиметров 10? Как это будет выглядеть на стене? Нужно убрать из зеркала всё то, что не относится к непосредственному отображению информации.

Вместо листа фанеры (или, точнее, чего-то напоминающего ДСП), закрывающего трубу в углу у входной двери, делаем гипсокартонную конструкцию с инспекционными лючками. От щитка внутрь конструкции проводим розетку, прячем туда блок питания и одноплатник. Для активной вентиляции ставим тихий производительный вентилятор. Таким образом, всё что может увеличивать громоздкость зеркала — убираем с глаз долой, заодно обеспечив активному оборудованию какое-никакое охлаждение.

Понеслась

Сам ремонт, отношения с ремонтниками, соседями, правоохранительными органами — не тема для Хабра. Но надо понимать, что стоимость всех тех решений, которые принимались в отношении зеркала по сравнению со стоимостью самого ремонта — тьфу, наплевать и растереть. Опять же, на хобби денег не так жалко.

Небольшое отступление

Все упоминания и ссылки на товары и производителей даны в целях беззастенчивой рекламы и обогащения просто на основе собственного опыта. Если захотите воспользоваться — ваше право. Не захотите — ваше дело.

  1. Зеркало. Заказал полотно «Гизелла» 400×1000 мм на Московской зеркальной фабрике. Я как раз туда поехал заказать зеркало и полочку в ванную. Когда рассказал менеджеру для чего мне такое зеркало, он так вдохновился, что с разрешения своего начальника сделал скидку процентов в 20 от прайса. Вышло совсем не пугающе.

  2. Рама. Сначала вообще не понимал чего мне подойдёт. Нашёл салон с большим количеством образцов, смотался туда. Изучал образцы, разговаривал с мастером. Понял, что мне нужна рама с профилем типа «коробочка». Сделал заказ. Раму попросил усилить уголками для сохранения жёсткости конструкции, хотя она и не является несущей. Срок изготовления — неделя или около того. До кучи прикупил всякой фурнитуры — фиксаторы, зажимы, металлический трос, петли для троса.

  3. Монитор. Пошёл на Алиэкспресс искать увиденный мной монитор. Нашёл ещё лучше. 17.3» 2560×1440, доставка из Москвы. Заказал сразу же. Толщина 12 мм, вес — не помню уже, но очень лёгкий.

  4. Лист фанеры. Этот лист является несущей основой конструкции. Толщина 12 мм, чтобы в него аккуратно встроить монитор. И чтоб можно было вкручивать саморезы на достаточную глубину, но ещё не достать до зеркала. В тот момент в Мерлене в наличии было что-то ужасное. Кривая, не шлифованная, даже если написано обратное. Брать наугад через интернет, да ещё и без возможности распила — ну его такое развлечение. Поехал в Максидом, посмотрел на экспортную. Очень понравилось. Взял, на месте выпилили кусок нужного размера.

  5. Одноплатник. Тут у меня сомнений не было вовсе. Nvidia Jetson Nano. Малина плюс ядра Cuda — отличная комбинация. После того как человек у которого я его покупал, дал вдогонку пару камер со шлейфами, у меня возникла очередная гениальная мысль. Но об этом — потом.

  6. Блок питания. Монитору для питания нужно Type C, Джетсону — Micro USB на 2А. Что же купить?… Что-что? Ugreen GaN на 65Вт. Раньше купил такого типа на 100Вт, использую как единственный в поездках для всего, в том числе — и ноутбука. Очень компактный, достаточно увесистый. Никаких фиксированных кабелей. Сначала пошёл было смотреть на алиэкспессе, потом сравнил цену на Я.маркете и понял что на маркете — дешевле. Так что взял не раздумывая. Проверил под нагрузкой — не греется совершенно.

  7. Вентилятор. Надеюсь, он не понадобится, но пусть будет. В комплекте Джетсона мне дали 3-сантиметроый Noctua под штатное крепление на радиатор. Но он с такими размерами всё равно слишком для меня шумный. Плюс, добавлял 20 мм высоты всей конструкции, что весьма ощутимо. Посмотрел цену 12-сантиметровых Noctua, загрустил. Взял be quiet! Pure Wings 2 120mm PWM. Тем более, что вентиляторы этого производителя уже использовал, был доволен.

  8. Баллончик чёрного матового лака — чтобы покрасить ту сторону фанеры, которая обращена к зеркалу. Только на чёрном фоне зеркало является зеркалом. Если в кино вы видите, что детективы по ту сторону полупрозрачного зеркала стоят в освещённом помещении — это враки, они должны сидеть в кромешной тьме.

  9. Крепление на стену. Исходил из следующего: Плотность фанеры где-то около 8 кг/м2, так что мой кусок весит примерно 3.2 кг. Вычесть вес вырезаного фрагмента, добавить вес монитора и рамы. Итого, скажем, 4 кг. Ещё килограмм 6 добавляет стекло. Всего — 10. Этот вес работает на срез, так что я использовал пару дюбелей Driva — чтобы уж подстраховаться. Следов деформации гипсокартона вроде пока нет.

  10. Всякое по мелочи. Латунные стойки для крепления Джетсона к дверце. Монтажные отверстия на плате рассчитаны на винты М2.5, стойки с подходящей резьбой нашёл на Чип и Дип. Винты М2.5 я взял от раскрученных старых винчестеров. Так что вы думаете? Из 4 заказанных стоек нормальная резьба была только у 3. В четвёртой винты проваливались. Пришлось колхозить. Карту памяти взял какую-то Samsung на 64ГБ, надеюсь, выживет.

Установил на Джетсон версию Убунты от Nvidia (там 18.04 с драйверами и обоями от Nvidia). Подумал, что не буду экспериментировать и ставить 22.04. Обои убрал, сделал ровную чёрную заливку. Поставил Python 3.11 и Nodejs.

Рассчитал, где должен находиться монитор с учётом высоты, на которой будет вешаться зеркало и уровня глаз жены. Начертил чертёж и вместе с листом фанеры отдал в знакомую мастерскую, где прорезали нужное мне отверстие. Когда принёс лист домой и вложил в вырез монитор, поразился насколько аккуратно и с минимальным зазором получилось.

Процесс примерки

Процесс примерки

Собрал сэндвич. Рама кладётся лицом на стол, в соответствующие пазы укладывается зеркальное полотно (не перепутать какой стороной!). На полотно укладывается лист фанеры. Фанера фиксируется на раме прижимами для подрамника. В вырез в фанере укладывается монитор (лицом вниз!). Монитор фиксируется на фанере пружинными зажимами. На нужной высоте к фанере прикручиваются 2 пластины с D-кольцом — для троса. Прикрепляется стальной трос — для подвешивания.

Зачем-то покрасил обратную сторону в

Зачем-то покрасил обратную сторону в «серебро с эффектом кракелюра». Следующий раз просто покрою прозрачным глянцевым лаком

Зеркало в сборе поднимается со стола, ставится на какую-нибудь подставку и придирчиво осматривается. Видно, что в том месте, где предусмотрен вырез под коннекторы для подключения монитора, зеркало просвечивает. Срочно ищется лист чёрного поролона, чтобы подложить под коннекторы. Получается идеально. До окончания ремонта зеркало в сборе убирается.

Тем временем нужно писать бэк-енд для погоды.

И о погоде

В качестве источника взял tomorrow.io. А что, API у них простой, бесплатные лимиты вполне пристойные. Запрос раз в 10 минут даёт меньше 150 запросов в сутки — при лимите в 500. Так что если нужно, можно запрашивать данные аж до 3 географических мест.

Небольшое отступление

Без косяков не обошлось

Причём, всё в духе моей старой статьи. В запросе можно указать в каком часовом поясе возвращать время. При этом, указываю ли я auto, или Europe/Moscow — все времена возвращаются с указанием корректного часового пояса, например 2023-05-19T06:00:00+03:00, кроме времени восхода/заката солнца, например 2023-05-19T01:27:00Z — что непонятно. Да, конечно, время-то правильное, с поправкой на часовой пояс. Но почему не возвращается в том же часовом поясе, что и всё остальное? Именно с этими временами дальше начинаются проблемы. Загрузить в БД? Чёрта с два, MySQL считает что это местный часовой пояс. При этом, если загрузить 2023-05-19T01:27:00+00:00, то в базу ложатся уже правильные значения.

Сначала скорректировал соответствующими функциями SQL. Предварительно, конечно, пришлось загрузить в MySQL базу tzinfo. Получилось, данные в БД стали класться верно. Потом я подумал, ну хорошо. А если tomorrow в какой-то момент это поправит или я начну использовать другой сервис в котором не будет этой проблемы, а будут какие-нибудь другие, мне что, это назад чинить?

Вернул SQL INSERT как было первоначально, пошёл разбираться как это сделать питоном поближе к поставщику. Дочитал до функции datetime.fromisoformat . То что доктор прописал! Стал смотреть на реализации в разных версиях. Да чтоб тебя. На компьютере, где разрабатывается — 3.9. На сервере, где планируется запускать код — вообще 3.8. И естественно, в обеих этих версиях код из примера

>>> datetime.fromisoformat('2011-11-04T00:05:23Z')
datetime.datetime(2011, 11, 4, 0, 5, 23, tzinfo=datetime.timezone.utc)

выбрасывает исключение. Что делать, что делать? Обернул это в функцию, стал сравнивать с номером версии питона. Подумал, что так не годится, написал вот такой костыль:

def tz2tz(ts):
    dt = None
    try:
        dt = datetime.fromisoformat(ts)
    except:
        pass
    if dt is not None:
        return '{0}'.format(dt.astimezone(pytz.timezone('Europe/Moscow')))
    
    if ts[-1] == 'Z':
        return ts[:-1] + '+00:00'

    return None

Выглядит ужасно, но свою задачу выполняет. Тут, конечно, можно избавиться от прописанного в коде часового пояса, да и вообще переписать покрасивее, но пока наплевать.

Стал пытаться понять, есть ли какая-то логика в их кодировании погодных условий. И так смотрел на их таблицу Weather Codes, и эдак, но никакой логики не увидел. Поэтому решил не базироваться на их кодах, а придумать свои, с блекджеком и метеорологами. Вот начало таблицы кодов из моей вики:

26703f1dd87a5f66d975b666dafe66a4.png

Всё строго и логично. Для трансляции кодов от конкретного поставщика в собственные делаем таблицу отображения в БД и при загрузке данных в базу накладываем заклинание примерно вот такого вида:

INSERT INTO `weather-hourly` (`location-id`, `provider-id`, `request-ts`, `ts`, `temp`, `condition-code`)
VALUES (%s, %s, %s, %s, %s, (SELECT `condition-code` FROM `condition-map` WHERE `provider-id` = %s AND `provider-code` = %s))

Вложенный SELECT — это как раз оно. Опять же, решив сменить поставщика, просто расширяем таблицу condition-map его условиями, больше нигде ничего не меняя. Красота!

И конечно же, пришлось нарисовать целую кучу SVG-иконок, даже для той погоды, что в Москве никогда не случится.

В результате, бэк-енд крутится, один агент берёт погоду и кладёт её в БД, другой — извлекает её из БД и отправляет в MQTT, а третий — обрабатывает запросы при инициализации потребителей. mosquitto_pub и mosquitto_sub не дадут соврать. Пора переходить к фронт-энду.

Показываем

Развёртывание MagicMirror проблем не вызывает, скорее дело в выборе готовых модулей и написании недостающих.

Пока начинал писать отображение погоды, много смотрел на стандартные часы и календарь. В часах мне не понравилась организация информации, размер и цвет шрифта (#999). Глаза что у меня, что у жены уже не те, а нужно чтобы и без очков информация считывалась сразу же. Набросал собственный лэйаут. Протестировал на жене, получил отзыв что так гораздо лучше. Вот и хорошо, часы напишу собственные, объединю с текущей погодой. В результате, ни одного стандартного модуля не оставил. Всё сам, всё сам.

Кроме того, очень не понравилось что в стандартных модулях объект DOM обновляется полностью каждый раз, когда необходимо туда что-то записать. Спрашивается, нафига? Сделал так: объект DOM для модуля создаётся один раз при инициализации, а дальше — по обстоятельствам. Если зеркало зачем-то хочет обновить экран, ему возвращается этот объект. Содержимое в нём уже есть. Нужно обновить часы — не вопрос:

updateClock: function()
{
    const now = moment();

    this.dow.innerHTML = now.format('dddd');
    this.date.innerHTML = now.format('D MMMM Y');
    this.time.innerHTML = now.format('HH:mm');
},

Нужно обновить погоду — пожалуйста:

displayCurrentWeather: function(data)
{
    ...
    this.icon.src = this.config.iconPath + '/' + data[0].icon;
    this.temp.innerHTML = Math.round(data[0].temp) + '°';
},

Примерно таким же образом поступил и с модулем ежедневного прогноза погоды.

Вот так оно и работает

Вот так оно и работает

Попробовал отдавать погодные иконки непосредственно с флешки, на которой всё работает. Это оказалось неприемлемо. Если иконка не в кэше, доступ к ней мог занимать 5–6 секунд. Перенёс иконки на домашний веб-сервер, поправил конфиг модуля — иконки стали отдаваться за пару миллисекунд.

Для взаимодействия с бэк-ендом выбирал между 2 готовыми мостами «MQTT <=> внутренние сообщения». Выбрал MMM-MQTTbridge. Оба делают одно и то же, но этот — менее замороченный. Ну и что из того что в репозитарии нет активности уже 3 года? Котики, вон, тоже не эволюционируют потому что сразу получились идеальными.

Итого

f86d73227b46ef884ba28dd2cb5c66db.jpg

Зеркало висит, работает как зеркалом, так и часами с погодной станцией. Жена довольна, гости офигевают, начинают хотеть себе такое же. Но когда узнают, что это только вершина айсберга и чтоб что-то такое повторить нужно быть мной и вбухать изрядно денег и времени — успокаиваются.

Мозги...

Мозги…

И что, на этом — всё?

Ничего подобного

Конечно, нет. Как минимум, в дополнение к календарю с праздниками и днями рождения нужно сделать напоминалку. Оплатить коммуналку, хостинг, какие-то ещё события, да мало ли чего.

Больше всего идей, конечно же, опять с погодой. Хочу купить Zigbee-MQTT шлюз и пару датчиков температуры. И выводить реальную температуру за окном. Ну и сохранять, естественно же. Единственно, пока не попадалось датчиков, обеспечивающих работоспособность ниже -20С. Если знаете такие, скажите.

Надо будет повозиться с почасовыми прогнозами. Мне принципиально хочется отобразить их в виде диаграммы, но обязательно нанести на неё значки погоды. Но не на каждый отсчёт, потому что будет совершенно нечитаемая мешанина. А только на момент изменения. Для визуализации мне очень нравится HighCharts, я его использовал и на планшете. С тех пор прошло довольно много версий, и теперь с его помощью можно сделать вот так:

Вот здесь как раз мешанина из иконок потому что не нужно повторять дублирующиеся

Вот здесь как раз мешанина из иконок потому что не нужно повторять дублирующиеся

Не обращайте внимания на значки погоды, я их хочу отобразить не там где они сейчас, а снизу, где направление ветра. И только изменения.

Совершенно точно не собираюсь останавливаться на одном провайдере погоды, раз уж вся система масштабируема. Из нескольких провайдеров начинать выбирать правльного. Например, тот у кого на определённом отрезке времени прогноз меньше отличается от его же текущих данных, назначается доверенным. Можно сравнивать с реальными показателями. Для такого особо много писать не придётся. Можно на исторических данных начать играть в нейросети и вообще строить свои собственные прогнозы. Тут-то Cuda и пригодится.

Пока не решил как поступить с планшетом. Скорее всего, погоду оттуда теперь уберу, надо придумать как использовать место и интерактивность.

Скорее всего, в какой-то момент задумаюсь об апгрейде монитора в зеркале. Уже сейчас появились в продаже мониторы на 18.5», не исключено что появятся и большие. Просто надо будет повторить — купить лист фанеры, вырезать отверстие под монитор, покрасить и пересобрать в раме. Возможно, при простом росте диагонали даже фанеру менять не придётся, просто вырезать пошире. Хуже другое. У ЖК-монитора в темноте заметна подсветка, при том что фон — чёрный. Мало того, засветы неравномерны и возможно меня это начнёт бесить. Скорее всего апгрейдиться придётся на OLED-монитор, главное чтобы они такие появились. Пока на алиэкспрессе ничего с диагональю больше 15 дюймов не встречал.

Думаю об использовании камеры. Если она будет нормально видеть сквозь зеркало, то можно будет прикрутить распознавание лиц. Если опознаёт меня — включить монитор по HDMI и вывести один контент, жену — другой. Если обоих — то всё равно мой. Пока никого рядом или кто-то незнакомый — монитор выключен и зеркало изображает из себя просто зеркало. А фото незнакомца по тихому отправляет мне в телегу. Снова Cuda пригодится. Эдакая охранная система до кучи.

Звук на монитор через HDMI нормально выводится и его даже неплохо слышно. Какой-то микрофон в мониторе тоже есть. По крайней мере, диагностика на голос реагирует. Добавить к системе какой-нибудь STT/TTS и можно пилить собственного голосового чат-бота. Интересно, «Свет мой» пойдёт в качестве триггера?

Неплохо было бы вывести состояние баланса мобильных телефонов, особенно родителей. Но в природе кажется не существует оператора который имел бы подходящее API для своих абонентов? Эй, МТС — у меня в семье все телефоны ваши (служебные, кстати, тоже), а вменяемого способа их контролировать нету.

Ещё надо будет разобраться с какой-то проблемой между драйверами NVidia и Электроном. В лог ошибок каждые несколько секунд добавляется запись Error: Can't initialize nvrm channel. Конкретно про мою ситуацию ничего не нагугливается, но есть что-то примерно похожее. Надо будет покопать. Мне бы наплевать на эту ошибку если всё работает, но это пространство на флешке, которое когда-нибудь закончится, а главное — её износ. Всё таки хотелось бы уменьшить количество записи.

© Habrahabr.ru