- Инженерный блог о self-hosted инфраструктуре/
- Статьи/
- Pico и метеостанция: мониторинг окружающей среды в тёплое время года/
Pico и метеостанция: мониторинг окружающей среды в тёплое время года

Содержание
Pico и метеостанция: мониторинг окружающей среды в тёплое время года #
Введение: Зачем DevOps-инженеру своя метеостанция #
Я собрал эту метеостанцию не ради экономии и не ради хайпа. Триггер был простым: интересно, могу ли я сам из говна и палок собрать, сколько это по трудозатратам, и будет ли это стабильно работать месяцами.
Результат — два одинаковых набора датчиков:
- Один дома в спальне (подключен к локальной сети)
- Второй на даче, в 60 км от Москвы (более автономный, с солнечной панелью)
Это эксперимент по мониторингу окружающей среды, который дополнил мой production K3s кластер на трех Raspberry Pi 5 — теперь я вижу не только CPU/RAM/диск, но и то, в каких условиях работает железо.
Философия: Моё железо — мои правила #
Почему не купить готовый Xiaomi/Aqara/Netatmo за 3-5 тысяч рублей?
Утечки данных. Каждое устройство шлёт телеметрию в облако вендора. Кто эти данные видит, как хранит, кому продаёт — я не контролирую.
Избыточная телеметрия. Готовые решения любят спамить пушами: “Слишком сухо, купите увлажнитель!”, “Температура высокая, включите кондиционер!”. Это цифровой шум. Моя система просто пишет данные в InfluxDB. Вот график. Смотри, анализируй, принимай решение сам.
Отсутствие контроля над режимами отправки. Я не могу заставить Xiaomi-термометр отправлять данные раз в час вместо раза в минуту. Я не могу переключить его на локальный MQTT-брокер без костылей.
Когда у тебя bare-metal Kubernetes кластер на RPi 5, ты не арендуешь инфраструктуру — ты ей владеешь. Нет vendor lock-in, нет геоблокировок, нет “мы изменили условия обслуживания”. Та же логика применима и к IoT.
Железо: Почему именно Pico W 2 и датчики SGP30 + DHT20 #
Микроконтроллер #
Raspberry Pi Pico W 2 — контроллер, с которым я уже был знаком. Не ESP32, не еще одна RPi. Просто Pico W, который я уже использовал в других проектах.
Почему не ESP32? У Pico W 2 есть Wi-Fi, и его достаточно для моих задач. Если бы я делал LoRa/Zigbee — взял бы другой чип. Но для MQTT поверх Wi-Fi Pico W подходит.
Датчики #
SGP30 — датчик качества воздуха (eCO2, TVOC). Показывает не просто “углекислый газ”, а эквивалент CO2 и летучие органические соединения. Требует калибровки и baseline.
DHT20 — температура и влажность внутри помещения. Простой, надёжный, дешёвый.
Локация: “Штатная вафля до сим-модема” #
Первый набор лежал на первом этаже, рядом с сим-модемом. Pico W 2 подключался к Wi-Fi, отправлял данные на MQTT-брокер, который находился на Orange Pi
Второй набор — на даче, в более автономной конфигурации с солнечной панелью. в 60 км от него.
Физика #
Длина проводов I2C — 5-10 см. Подтягивающие резисторы (pull-up) на шину I2C не понадобились. После калибровки значения стабилизировались, “плавающих” данных не было.
Программная часть: MicroPython → CircuitPython и грабли #
Почему CircuitPython, а не MicroPython? #
Изначально я писал код на MicroPython. Но MQTT-клиент не собирался нормально — либы не работали, или работали через костыли. Пришлось мигрировать на CircuitPython.
Грабли CircuitPython #
Работа с файлами как с флешкой. CircuitPython монтируется как USB-накопитель. Чтобы обновить код или добавить библиотеку, ты копируешь .mpy файлы на диск, как на флешку. Это удобно для прототипирования, но неудобно для продакшена.
Прямая корректная доставка либ. Не все библиотеки CircuitPython совместимы с MicroPython. Приходилось искать правильные версии, проверять зависимости, иногда форкать и патчить.
Утечки памяти при HTTP-запросах. Pico W 2 имеет ограниченную RAM. При парсинге JSON от Visual Crossing API случались MemoryError. Решение — принудительный gc.collect() перед и после запроса:
gc.collect() # Принудительно чистим RAM перед парсингом
try:
with requests_session.get(VC_URL) as response:
if response.status_code == 200:
weather = response.json()
# ... парсинг данных
except MemoryError:
print("MemoryError! Не хватает RAM при парсинге JSON.")
finally:
gc.collect() # Освобождаем память после закрытия сокета
Сохранение baseline SGP30 в NVM #
SGP30 требует 12 часов прогрева для калибровки baseline. Если Pico перезагружается, датчик снова тупит полдня. Решение — сохранять baseline в NVM (Non-Volatile Memory):
def save_baseline_to_nvm(eco2, tvoc):
try:
data = [
(eco2 >> 8) & 0xFF, # старший байт eCO2
eco2 & 0xFF, # младший байт eCO2
(tvoc >> 8) & 0xFF, # старший байт TVOC
tvoc & 0xFF, # младший байт TVOC
]
microcontroller.nvm[0:4] = data
print(f"Baseline сохранён в NVM: eCO2=0x{eco2:04X}, TVOC=0x{tvoc:04X}")
except Exception as e:
print("Ошибка записи в NVM:", e)
def load_baseline_from_nvm():
try:
data = microcontroller.nvm[0:4]
eco2 = (data[0] << 8) | data[1]
tvoc = (data[2] << 8) | data[3]
if eco2 == 0xFFFF or tvoc == 0xFFFF or (eco2 == 0 and tvoc == 0):
return None, None
print(f"Baseline загружен из NVM: eCO2=0x{eco2:04X}, TVOC=0x{tvoc:04X}")
return eco2, tvoc
except Exception as e:
print("Ошибка чтения из NVM:", e)
return None, None
Световая сигнализация и Watchdog: Смерть = Перезагрузка #
Чтобы постоянно не лезть в консоль и не смотреть логи, я сделал LED-сигнализацию:
led = digitalio.DigitalInOut(LED_PIN)
led.direction = digitalio.Direction.OUTPUT
def blink_pattern(pattern):
for duration in pattern:
led.value = 1
time.sleep(duration)
led.value = 0
time.sleep(0.2)
def signal_wifi_error():
blink_pattern([0.1] * 4) # 4 коротких вспышки
def signal_mqtt_error():
blink_pattern([0.1] * 4 + [0.5]) # 4 коротких + 1 длинная
def signal_ok():
blink_pattern([0.5]) # 1 длинная вспышка — всё ок
Теперь я вижу статус устройства по миганию светодиода:
- 4 коротких вспышки — ошибка Wi-Fi
- 4 коротких + 1 длинная — ошибка MQTT
- 1 длинная вспышка — всё ок
Главное правило удаленного IoT: Смерть = Перезагрузка. В коде нет сложных алгоритмов восстановления после обрыва связи. Если Wi-Fi отвалился или скрипт ушел в бесконечный цикл, устройство просто ребутится (срабатывает Watchdog или стандартное поведение CircuitPython при необработанных исключениях).
Когда устройство стоит в 60 км от тебя, ты не можешь нажать кнопку Reset. Единственный способ выжить — это уметь падать и подниматься самостоятельно. Моргающий диод нужен мне не для того, чтобы я смотрел на него каждый день, а чтобы при редких визитах на дачу за 3 секунды понять: «Ага, отвалился роутер, нужно перезагрузить модем».
Архитектура данных: От датчика до Grafana #
SGP30 + DHT20] -->|MQTT
JSON| B[Mosquitto Broker
Docker на Orange Pi] B --> C[Telegraf
Docker на Orange Pi] C -->|InfluxDB Line Protocol| D[InfluxDB] D --> E[K3s кластер
RPi 5] E --> F[Grafana
Визуализация] style A fill:#2563eb,stroke:#1e40af,color:#fff style B fill:#16a34a,stroke:#15803d,color:#fff style C fill:#16a34a,stroke:#15803d,color:#fff style D fill:#ea580c,stroke:#c2410c,color:#fff style E fill:#2563eb,stroke:#1e40af,color:#fff style F fill:#dc2626,stroke:#991b1b,color:#fff
(Orange Pi) participant TG as Telegraf participant DB as InfluxDB participant GF as Grafana Pico->>SGP30: Чтение eCO2, TVOC SGP30-->>Pico: 450 ppm, 120 ppb Pico->>DHT: Чтение температуры/влажности DHT-->>Pico: 24.5°C, 45% alt Раз в час Pico->>VC: GET /timeline/today VC-->>Pico: JSON {temp, humidity, pressure...} end Pico->>Pico: Формирование JSON payload Pico->>MQTT: PUBLISH sensors/pico_w_env_01 MQTT->>TG: MQTT Consumer TG->>TG: JSON → InfluxDB Line Protocol TG->>DB: WRITE indoor_temp=24.5,co2=450... Note over GF: Пользователь открывает браузер GF->>DB: SELECT query (InfluxQL/Flux) DB-->>GF: Time-series data GF-->>GF: Отрисовка графиков
Архитектурный компромисс: MQTT без шифрования через DDNS #
Если вы посмотрите на мой код, то увидите параметр is_ssl=False. Да, я передаю телеметрию с дачи на домашний сервер через публичный интернет (через DDNS) в открытом виде.
Почему не TLS? Настройка сертификатов и шифрования на микроконтроллере с 2 МБ RAM — это отдельная боль. TLS-хендшейк съедает память, а обновление сертификатов на удаленном устройстве, которое питается от солнечной панели, превращается в ад.
Я принял осознанное решение: мой провайдер будет знать, что в 14:00 на даче было +24°C и 450 ppm CO2. Это не гостайна. В мире IoT иногда приходится жертвовать паранойей ради стабильности и сохранения драгоценных миллиампер.
Telegraf: парсинг MQTT #
Telegraf крутится в Docker на Orange Pi. Конфиг для парсинга MQTT:
[[inputs.mqtt_consumer]]
servers = ["tcp://localhost:1883"]
topics = [
"sensors/pico_w_env_01"
]
username = "mqtt_user"
password = "mqtt_pass"
data_format = "json"
json_string_fields = ["indoor_temperature", "indoor_humidity", "indoor_co2", "indoor_tvoc"]
tag_keys = ["device_id"]
Telegraf парсит JSON, преобразует в InfluxDB Line Protocol и пишет в базу.
InfluxDB: хранение временных рядов #
Данные хранятся в InfluxDB с тегами:
device_id— идентификатор устройства (pico_w_env_01)location— локация (home/dacha)
И полями:
temperature,humidity,co2,tvoc— внутренние данныеtemperature_out,humidity_out,pressure— внешняя погода
Grafana: визуализация без алертов #
Grafana просто рисует графики. Никаких алертов, никаких пушей. Просто “красная зона” на графике CO2 > 1000 ppm. Ты сам смотришь на график и решаешь: пора проветривать или ещё рано.
Внешние данные: OpenWeatherMap → Visual Crossing (РКН) #
Изначально: OpenWeatherMap #
Сначала я использовал OpenWeatherMap для получения уличной погоды:
- Температура
- Влажность
- Давление
- Ветер
- Облачность
Pico раз в час запрашивал данные, упаковывал с локальными измерениями и отправлял по MQTT.
Проблема: РКН #
Через несколько месяцев API OpenWeatherMap стало нестабильным. Либо РКН заблокировал домен, либо API стало отдавать 403. Pico просто не мог зарезолвить DNS или получал ошибку при запросе.
Решение: Visual Crossing #
Перешёл на Visual Crossing API. Работает стабильно, требует API-ключ, но данные полные:
VC_URL = f"https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/{VC_LOCATION}/today?unitGroup=metric&include=current&key={VC_API_KEY}&contentType=json"
Запрос возвращает JSON с текущими условиями, который парсится и добавляется к локальным данным:
outdoor = {
"temperature_out": c.get("temp"),
"humidity_out": c.get("humidity"),
"pressure": c.get("pressure"),
"precip": c.get("precip"),
"solarradiation": c.get("solarradiation"),
"wind_speed": c.get("windspeed"),
"wind_dir": c.get("winddir"),
"cloudcover": c.get("cloudcover")
}
Эксперимент по энергоэффективности: Почему АКБ умер за 3 дня #
Гипотеза #
Потянет ли Pico W 2 с АКБ + buck-boost + солнечной панелью (ALLPOWERS 21W пиково) в 60 км от Москвы?
Панель ориентирована на юго-запад. Выдаёт 0.5А в сумраке, до 5А на солнышке.
Ловушка time.sleep(): Почему Pico W на самом деле не спит #
В моем коде вы не найдете вызова microcontroller.deep_sleep(). В конце цикла стоит обычный time.sleep(0.2). Это значит, что микроконтроллер никогда не выключается. Он постоянно находится в режиме idle, а Wi-Fi модуль непрерывно шлет keep-alive пакеты роутеру, чтобы не потерять ассоциацию.
Почему я не использую настоящий Deep Sleep? При выходе из глубокого сна на Pico W Wi-Fi стек инициализируется заново. Это занимает несколько секунд и вызывает резкий скачок потребления (до 150-200 мА), который может мгновенно просадить напряжение на слабой солнечной панели или вызвать brownout (перезагрузку от нехватки питания).
Поэтому 0.1 мА — это не сон, это «дремота» с включенным Wi-Fi. Для автономности это компромисс, но для стабильности связи — необходимость.
Реальность #
Первая итерация:
- Без пакетов: ~0.05-0.1 мА
- С пакетами (запрос/передача): пиково ~0.2-0.3 мА
- Итог: АКБ умер за ~3 дня (за 10 дней — в ноль)
Корневая причина: Pico W не умеет в нормальный Wi-Fi Deep Sleep. Wi-Fi жрёт 25-50 мА, постоянные keep-alive пакеты. Солнечная панель не успевает компенсировать расход, особенно в пасмурные дни.
Вторая итерация:
- Два АКБ по 1800 мАч (18650 Li-Ion)
- Результат: 14 дней автономии, потом дозарядка от солнца
(Об этом подробнее расскажу в статье про развитие метеостанции — повербанк, солнечная панель, автономность)
Выводы #
Wi-Fi для автономного IoT — это компромисс. Если нужна реальная автономность на месяцах, нужен другой стек:
- LoRa для дальней связи с низким энергопотреблением
- Zigbee для локальной mesh-сети
- ESP32 с гибким sleep-режимом
Инсайты: Контроль vs грабли #
Главная мысль #
Свой IoT — это всегда компромисс между контролем и граблями.
Ты получаешь полный контроль над данными, над режимами отправки, над инфраструктурой. Но ты тратишь время на:
- Подбор совместимых библиотек
- Отладку утечек памяти
- Борьбу с блокировками API
- Эксперименты с энергопотреблением
Готовое решение из коробки работает сразу. Но оно шлёт данные в облако вендора, спамит пушами и не даёт тебе контроля.
Вторая мысль #
Мониторинг среды так же важен, как мониторинг CPU на серверах.
Что я увидел на графиках в Grafana, чего никогда не показал бы готовый Xiaomi-термометр:
- Как быстро задыхаюсь ночью. CO2 в спальне поднимается до 1500 ppm за 4-5 часов сна. Это объясняет утреннюю вялость.
- Качество воздуха. TVOC показывает, когда пора проветривать после готовки или уборки.
- Тепловая инерция дома. Как быстро дом нагревается днём и остывает ночью.
- Корреляция между CO2 и самочувствием. Я стал проветривать чаще, и это реально помогло.
Цифровой шум #
Моя система не спамит пушами. Она просто пишет данные в InfluxDB. Ты сам смотришь на график и решаешь: пора открывать окна или ещё рано.
Это дзен-подход: вот данные, посмотри график, принимай решение, сам думай.
Что дальше: Планы на зиму #
Проблема: литий на морозе #
Литиевые АКБ нельзя заряжать при температуре ниже 0°C — происходит кристаллизация лития, батарея деградирует или взрывается.
На даче зимой температура падает ниже нуля. Солнечная панель продолжает заряжать АКБ, но это опасно.
Хорошая новость: аппаратная защита BMS #
Я проверил спецификации встроенного контроллера заряда. У него есть аппаратная защита LTC (Low Temperature Cutoff). Если датчик на самой плате контроллера фиксирует температуру ниже +1°C, он физически разрывает цепь заряда от солнечной панели.
Это значит, что кристаллизации лития и взрыва не произойдет. Батарея просто уйдет в спячку до весны. Плохая новость: зимой станция будет лежать мертвым грузом, пока солнце не поднимется выше и не нагреет бокс.
Варианты решения #
Утепление. Термобокс с подогревом от самой панели. Сложно, требует дополнительной электроники.
Переезд на LoRa/Zigbee. Снижение энергопотребления в 10-100 раз. Pico W → ESP32 + LoRa модуль. Данные отправляются раз в сутки, а не раз в час.
Солнечная панель большей мощности. Текущая 21W не справляется зимой, когда солнце низкое и дни короткие. Нужна панель 50-100W.
Отказ от беспроводной связи. Ethernet? Слишком сложно для дачи. Или просто отключать зарядку зимой через реле с термодатчиком.
Заключение #
Эта метеостанция — не про экономию денег. Это про инженерный контроль. Я знаю каждый кабель, каждый датчик, каждую строчку кода. Я понимаю, почему АКБ умер за 3 дня, и как это исправить.
Готовые решения проще. Но они не дают тебе знаний. Они дают тебе чёрный ящик, который шлёт данные в облако.
Мой подход: моё железо, мои правила. Даже если это требует времени на отладку, даже если АКБ умирает быстрее, чем я ожидал.
Зато я точно знаю: когда CO2 в спальне поднимается до 1500 ppm, пора открывать окно. И это знание я получил сам, а не из пуша “Купите увлажнитель!”.
💡 Исходный код и схемы: Весь код для CircuitPython (code.py, работа с NVM, парсинг JSON) и конфиги Telegraf лежат в открытом доступе. Ссылка на Репозиторий Github. Там всего пара файлов, но они выстраданы неделями отладки утечек памяти.
Связаться со мной: если у вас есть вопросы по инфраструктуре, предложения по сотрудничеству или просто хотите обсудить DevOps — пишите в Telegram. Канал: кейсы и разное. Отвечаю в течение 24–48 часов.