Initial commit

This commit is contained in:
Joan
2025-12-31 11:42:15 +01:00
parent 88d2bce132
commit 04cd96b1af
11 changed files with 638 additions and 0 deletions

16
.env.example Normal file
View File

@@ -0,0 +1,16 @@
# Home Assistant Configuration
HA_TOKEN=your_home_assistant_long_lived_access_token
HASS_URL=https://your-home-assistant-url.com
# Entity IDs
BRIGHTNESS_ENTITY_ID=sensor.your_brightness_sensor
WEATHER_ENTITY_ID=weather.forecast_home
INTERIOR_TEMP_ENTITY_ID=sensor.your_temperature_sensor
INTERIOR_HUMIDITY_ENTITY_ID=sensor.your_humidity_sensor
# Netdata Configuration
NETDATA_URL=http://your-netdata-host:19999
# LED Matrix Configuration
LED_ROWS=64
LED_COLS=64

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# Environment variables (contains secrets)
.env
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
# Virtual environments
venv/
.venv/
ENV/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db

26
config.py Normal file
View File

@@ -0,0 +1,26 @@
"""
Configuration module for Matrix64 LED display.
Loads environment variables from .env file.
"""
import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Home Assistant Configuration
HA_TOKEN = os.getenv('HA_TOKEN')
HASS_URL = os.getenv('HASS_URL')
# Entity IDs
BRIGHTNESS_ENTITY_ID = os.getenv('BRIGHTNESS_ENTITY_ID')
WEATHER_ENTITY_ID = os.getenv('WEATHER_ENTITY_ID')
INTERIOR_TEMP_ENTITY_ID = os.getenv('INTERIOR_TEMP_ENTITY_ID')
INTERIOR_HUMIDITY_ENTITY_ID = os.getenv('INTERIOR_HUMIDITY_ENTITY_ID')
# Netdata Configuration
NETDATA_URL = os.getenv('NETDATA_URL')
# LED Matrix Configuration
LED_ROWS = int(os.getenv('LED_ROWS', '64'))
LED_COLS = int(os.getenv('LED_COLS', '64'))

63
home_assistant.py Normal file
View File

@@ -0,0 +1,63 @@
"""
Home Assistant API client for Matrix64 LED display.
"""
import requests
from config import (
HA_TOKEN, HASS_URL,
BRIGHTNESS_ENTITY_ID, WEATHER_ENTITY_ID,
INTERIOR_TEMP_ENTITY_ID, INTERIOR_HUMIDITY_ENTITY_ID
)
def get_entity_value(entity_id):
"""Fetch entity state from Home Assistant."""
headers = {
"Authorization": f"Bearer {HA_TOKEN}",
"Content-Type": "application/json",
}
url = f"{HASS_URL}/api/states/{entity_id}"
try:
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
return response.json()
else:
print(f"Failed to retrieve entity {entity_id}: {response.text}")
return None
except Exception as e:
print(f"Error fetching {entity_id}: {e}")
return None
def get_weather():
"""Get weather attributes from Home Assistant."""
weather = get_entity_value(WEATHER_ENTITY_ID)
if weather:
return weather.get("attributes", {})
return {}
def get_weather_description():
"""Get current weather state/condition."""
weather = get_entity_value(WEATHER_ENTITY_ID)
if weather:
return weather.get("state", "unknown")
return "unknown"
def get_interior_weather():
"""Get interior temperature and humidity from BTH01-3132 sensor."""
temp_data = get_entity_value(INTERIOR_TEMP_ENTITY_ID)
humidity_data = get_entity_value(INTERIOR_HUMIDITY_ENTITY_ID)
temperature = temp_data.get("state") if temp_data else None
humidity = humidity_data.get("state") if humidity_data else None
return {"humidity": humidity, "temperature": temperature}
def get_brightness():
"""Get screen brightness value."""
brightness = get_entity_value(BRIGHTNESS_ENTITY_ID)
if brightness:
return brightness.get("state")
return None

56
install.sh Normal file
View File

@@ -0,0 +1,56 @@
#!/bin/bash
# Installation script for Matrix64 LED Display
# Run this once on your Raspberry Pi
set -e
INSTALL_DIR="/opt/matrix64"
REPO_URL="https://gitlab.kingstudio.es/jocaru/matrix64"
echo "=== Matrix64 LED Display Installation ==="
# Clone or update repository
if [ -d "$INSTALL_DIR" ]; then
echo "Directory exists, updating..."
cd "$INSTALL_DIR"
git pull origin master
else
echo "Cloning repository..."
git clone "$REPO_URL" "$INSTALL_DIR"
cd "$INSTALL_DIR"
fi
# Install Python dependencies
echo "Installing Python dependencies..."
pip3 install -r requirements.txt
# Create .env from example if not exists
if [ ! -f "$INSTALL_DIR/.env" ]; then
echo "Creating .env from template..."
cp "$INSTALL_DIR/.env.example" "$INSTALL_DIR/.env"
echo "IMPORTANT: Edit $INSTALL_DIR/.env with your actual values!"
fi
# Install systemd service
echo "Installing systemd service..."
cp "$INSTALL_DIR/matrix64.service" /etc/systemd/system/
systemctl daemon-reload
systemctl enable matrix64
# Make update script executable
chmod +x "$INSTALL_DIR/update.sh"
# Set up cron job for auto-updates (every 5 minutes)
CRON_JOB="*/5 * * * * $INSTALL_DIR/update.sh"
(crontab -l 2>/dev/null | grep -v "matrix64/update.sh"; echo "$CRON_JOB") | crontab -
echo ""
echo "=== Installation Complete ==="
echo ""
echo "Next steps:"
echo "1. Edit /opt/matrix64/.env with your Home Assistant token and URLs"
echo "2. Start the service: sudo systemctl start matrix64"
echo "3. Check status: sudo systemctl status matrix64"
echo "4. View logs: sudo journalctl -u matrix64 -f"
echo ""
echo "Auto-updates are enabled (every 5 minutes via cron)"

153
matrix.py Normal file
View File

@@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""
Matrix64 LED Display - Main Entry Point
Displays Home Assistant and Netdata data on a 64x64 LED matrix.
"""
from samplebase import SampleBase
from rgbmatrix import graphics
import time
import datetime
from config import LED_ROWS, LED_COLS
from weather_icons import draw_weather_icon
from home_assistant import (
get_weather, get_weather_description,
get_interior_weather, get_brightness
)
from netdata import get_hdd_temps
def get_temperature_color(temp):
"""Determine color based on temperature value."""
try:
temp_value = float(str(temp).replace('°C', '').strip())
if temp_value < 0:
return graphics.Color(0, 100, 255)
elif temp_value < 10:
return graphics.Color(100, 180, 255)
elif temp_value < 20:
return graphics.Color(0, 255, 100)
elif temp_value < 30:
return graphics.Color(255, 200, 0)
else:
return graphics.Color(255, 50, 50)
except ValueError:
return graphics.Color(255, 255, 255)
class Matrix64Display(SampleBase):
"""Main display class for the LED matrix."""
def __init__(self, *args, **kwargs):
super(Matrix64Display, self).__init__(*args, **kwargs)
self.parser.add_argument(
"-t", "--text",
help="The text to scroll on the RGB LED panel",
default="Hello world!"
)
self.temperature = None
self.humidity = None
self.interior_temperature = None
self.interior_humidity = None
self.last_update = time.time()
self.weather_desc = "unknown"
self.hdd_temps = None
def update_data(self):
"""Fetch all data from APIs."""
try:
# Weather data
weather = get_weather()
if weather.get("temperature") is not None:
self.temperature = f'{weather.get("temperature")}°C'
self.humidity = weather.get("humidity")
self.weather_desc = get_weather_description()
# Interior data
interior = get_interior_weather()
if interior.get("temperature") is not None:
self.interior_temperature = f'{interior.get("temperature")}°C'
self.interior_humidity = interior.get("humidity")
except Exception as e:
print(f"Error updating data: {e}")
def update_brightness(self):
"""Update LED matrix brightness."""
try:
brightness = get_brightness()
if brightness:
self.matrix.brightness = int(float(brightness)) / 10
except Exception as e:
self.matrix.brightness = 5
print(f"Error updating brightness: {e}")
def update_hdd_temps(self):
"""Update HDD temperatures."""
self.hdd_temps = get_hdd_temps()
def run(self):
"""Main display loop."""
canvas = self.matrix.CreateFrameCanvas()
# Load fonts
font = graphics.Font()
font.LoadFont("../../../fonts/7x13.bdf")
temp_font = graphics.Font()
temp_font.LoadFont("../../../fonts/5x8.bdf")
text_color = graphics.Color(20, 75, 200)
# Initial data fetch
self.update_brightness()
self.update_data()
self.update_hdd_temps()
while True:
now = datetime.datetime.now()
time_str = f"{now.hour:02d}:{now.minute:02d}:{now.second:02d}"
current_time = time.time()
# Update every 60 seconds
if current_time - self.last_update >= 60:
self.update_data()
self.update_brightness()
self.update_hdd_temps()
self.last_update = current_time
canvas.Clear()
# Time display
graphics.DrawText(canvas, font, 4, 42, text_color, time_str)
# Weather icon
draw_weather_icon(canvas, 0, 0, self.weather_desc)
# Outdoor temperature (right-aligned)
if self.temperature:
color = get_temperature_color(self.temperature)
length = graphics.DrawText(canvas, temp_font, 0, 0, color, self.temperature)
graphics.DrawText(canvas, temp_font, 64 - length, 8, color, self.temperature)
# Interior temperature (right-aligned)
if self.interior_temperature:
color = get_temperature_color(self.interior_temperature)
length = graphics.DrawText(canvas, temp_font, 0, 0, color, self.interior_temperature)
graphics.DrawText(canvas, temp_font, 64 - length, 16, color, self.interior_temperature)
# HDD temperatures
if self.hdd_temps and len(self.hdd_temps) > 4:
row1 = " ".join(str(t[0])[:2] for t in self.hdd_temps[1:4])
row2 = " ".join(str(t[0])[:2] for t in self.hdd_temps[4:])
graphics.DrawText(canvas, temp_font, 12, 52, text_color, row1)
graphics.DrawText(canvas, temp_font, 12, 60, text_color, row2)
time.sleep(0.5)
canvas = self.matrix.SwapOnVSync(canvas)
if __name__ == "__main__":
display = Matrix64Display()
if not display.process():
display.print_help()

17
matrix64.service Normal file
View File

@@ -0,0 +1,17 @@
[Unit]
Description=Matrix64 LED Display
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/matrix64
ExecStart=/usr/bin/python3 /opt/matrix64/matrix.py --led-rows=64 --led-cols=64
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target

17
netdata.py Normal file
View File

@@ -0,0 +1,17 @@
"""
Netdata API client for Matrix64 LED display.
"""
import requests
from config import NETDATA_URL
def get_hdd_temps():
"""Get HDD temperatures from Netdata."""
try:
url = f"{NETDATA_URL}/api/v2/data?contexts=hddtemp.disk_temperature&points=1&group_by=instance"
response = requests.get(url, timeout=10)
hdd_temps = response.json()['result']['data'][0]
return hdd_temps
except Exception as e:
print(f"Error fetching HDD temps: {e}")
return None

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
python-dotenv>=1.0.0
requests>=2.28.0

29
update.sh Normal file
View File

@@ -0,0 +1,29 @@
#!/bin/bash
# Update script for Matrix64 LED Display
# Pulls latest changes from git and restarts service if updated
INSTALL_DIR="/opt/matrix64"
LOGFILE="/var/log/matrix64-update.log"
cd "$INSTALL_DIR" || exit 1
# Fetch latest changes
git fetch origin
# Check if there are updates
LOCAL=$(git rev-parse HEAD)
REMOTE=$(git rev-parse origin/master)
if [ "$LOCAL" != "$REMOTE" ]; then
echo "$(date): Updates found, pulling changes..." >> "$LOGFILE"
git pull origin master >> "$LOGFILE" 2>&1
# Install any new dependencies
pip3 install -r requirements.txt >> "$LOGFILE" 2>&1
# Restart the service
systemctl restart matrix64
echo "$(date): Service restarted" >> "$LOGFILE"
else
echo "$(date): No updates" >> "$LOGFILE"
fi

236
weather_icons.py Normal file
View File

@@ -0,0 +1,236 @@
"""
Weather icon drawing functions for 64x64 LED matrix.
All icons are designed for a 24x24 pixel area.
"""
from rgbmatrix import graphics
def draw_sunny(canvas, x, y):
"""Draw a sun with rays - clear/sunny weather."""
sun_color = graphics.Color(255, 200, 0)
ray_color = graphics.Color(255, 140, 0)
# Draw sun rays (8 directions)
graphics.DrawLine(canvas, x + 12, y + 2, x + 12, y + 5, ray_color)
graphics.DrawLine(canvas, x + 12, y + 19, x + 12, y + 22, ray_color)
graphics.DrawLine(canvas, x + 2, y + 12, x + 5, y + 12, ray_color)
graphics.DrawLine(canvas, x + 19, y + 12, x + 22, y + 12, ray_color)
graphics.DrawLine(canvas, x + 5, y + 5, x + 7, y + 7, ray_color)
graphics.DrawLine(canvas, x + 17, y + 17, x + 19, y + 19, ray_color)
graphics.DrawLine(canvas, x + 5, y + 19, x + 7, y + 17, ray_color)
graphics.DrawLine(canvas, x + 17, y + 7, x + 19, y + 5, ray_color)
# Draw filled sun
for r in range(6, 0, -1):
graphics.DrawCircle(canvas, x + 12, y + 12, r, sun_color)
def draw_clear_night(canvas, x, y):
"""Draw a crescent moon with stars."""
moon_color = graphics.Color(255, 255, 200)
# Draw moon
for r in range(7, 0, -1):
graphics.DrawCircle(canvas, x + 10, y + 12, r, moon_color)
# Draw stars
canvas.SetPixel(x + 20, y + 6, 255, 255, 255)
canvas.SetPixel(x + 18, y + 10, 255, 255, 255)
canvas.SetPixel(x + 22, y + 14, 255, 255, 255)
canvas.SetPixel(x + 19, y + 18, 255, 255, 255)
def _draw_cloud_shape(canvas, x, y, color):
"""Helper to draw a cloud shape."""
for r in range(4, 0, -1):
graphics.DrawCircle(canvas, x + 4, y + 4, r, color)
for r in range(5, 0, -1):
graphics.DrawCircle(canvas, x + 10, y + 3, r, color)
for r in range(4, 0, -1):
graphics.DrawCircle(canvas, x + 15, y + 5, r, color)
for r in range(3, 0, -1):
graphics.DrawCircle(canvas, x + 6, y + 7, r, color)
for r in range(3, 0, -1):
graphics.DrawCircle(canvas, x + 12, y + 7, r, color)
def draw_partly_cloudy(canvas, x, y):
"""Draw sun partially covered by cloud."""
sun_color = graphics.Color(255, 200, 0)
ray_color = graphics.Color(255, 140, 0)
cloud_color = graphics.Color(220, 220, 220)
# Draw partial sun
graphics.DrawLine(canvas, x + 8, y + 2, x + 8, y + 4, ray_color)
graphics.DrawLine(canvas, x + 2, y + 8, x + 4, y + 8, ray_color)
graphics.DrawLine(canvas, x + 3, y + 3, x + 5, y + 5, ray_color)
for r in range(4, 0, -1):
graphics.DrawCircle(canvas, x + 8, y + 8, r, sun_color)
# Draw cloud
_draw_cloud_shape(canvas, x + 6, y + 10, cloud_color)
def draw_partly_cloudy_night(canvas, x, y):
"""Draw moon partially covered by cloud."""
moon_color = graphics.Color(255, 255, 200)
cloud_color = graphics.Color(180, 180, 200)
for r in range(4, 0, -1):
graphics.DrawCircle(canvas, x + 8, y + 8, r, moon_color)
_draw_cloud_shape(canvas, x + 6, y + 10, cloud_color)
def draw_cloudy(canvas, x, y):
"""Draw overcast clouds."""
cloud_light = graphics.Color(200, 200, 200)
cloud_dark = graphics.Color(150, 150, 150)
_draw_cloud_shape(canvas, x + 2, y + 6, cloud_dark)
_draw_cloud_shape(canvas, x + 6, y + 10, cloud_light)
def draw_rainy(canvas, x, y):
"""Draw cloud with rain drops."""
cloud_color = graphics.Color(150, 150, 170)
rain_color = graphics.Color(100, 150, 255)
_draw_cloud_shape(canvas, x + 4, y + 4, cloud_color)
for i in range(4):
x_start = x + 6 + i * 4
graphics.DrawLine(canvas, x_start, y + 16, x_start - 2, y + 20, rain_color)
graphics.DrawLine(canvas, x_start + 1, y + 18, x_start - 1, y + 22, rain_color)
def draw_pouring(canvas, x, y):
"""Draw cloud with heavy rain."""
cloud_color = graphics.Color(100, 100, 120)
rain_color = graphics.Color(80, 130, 255)
_draw_cloud_shape(canvas, x + 4, y + 2, cloud_color)
for i in range(5):
x_start = x + 4 + i * 4
graphics.DrawLine(canvas, x_start, y + 14, x_start - 3, y + 22, rain_color)
def draw_snowy(canvas, x, y):
"""Draw cloud with snowflakes."""
cloud_color = graphics.Color(180, 180, 200)
_draw_cloud_shape(canvas, x + 4, y + 4, cloud_color)
for i in range(3):
sx = x + 8 + i * 5
sy = y + 18 + (i % 2) * 3
canvas.SetPixel(sx, sy, 255, 255, 255)
canvas.SetPixel(sx - 1, sy, 255, 255, 255)
canvas.SetPixel(sx + 1, sy, 255, 255, 255)
canvas.SetPixel(sx, sy - 1, 255, 255, 255)
canvas.SetPixel(sx, sy + 1, 255, 255, 255)
def draw_thunderstorm(canvas, x, y):
"""Draw cloud with lightning bolt."""
cloud_color = graphics.Color(80, 80, 100)
lightning_color = graphics.Color(255, 255, 0)
_draw_cloud_shape(canvas, x + 4, y + 2, cloud_color)
graphics.DrawLine(canvas, x + 12, y + 12, x + 10, y + 16, lightning_color)
graphics.DrawLine(canvas, x + 10, y + 16, x + 14, y + 16, lightning_color)
graphics.DrawLine(canvas, x + 14, y + 16, x + 10, y + 22, lightning_color)
def draw_foggy(canvas, x, y):
"""Draw horizontal fog lines."""
fog_color = graphics.Color(180, 180, 180)
for i in range(5):
y_pos = y + 6 + i * 3
graphics.DrawLine(canvas, x + 2, y_pos, x + 20, y_pos, fog_color)
def draw_windy(canvas, x, y):
"""Draw wind lines."""
wind_color = graphics.Color(150, 200, 255)
graphics.DrawLine(canvas, x + 2, y + 8, x + 18, y + 8, wind_color)
graphics.DrawLine(canvas, x + 18, y + 8, x + 20, y + 6, wind_color)
graphics.DrawLine(canvas, x + 4, y + 12, x + 22, y + 12, wind_color)
graphics.DrawLine(canvas, x + 22, y + 12, x + 23, y + 10, wind_color)
graphics.DrawLine(canvas, x + 2, y + 16, x + 16, y + 16, wind_color)
graphics.DrawLine(canvas, x + 16, y + 16, x + 18, y + 14, wind_color)
def draw_hail(canvas, x, y):
"""Draw cloud with hail."""
cloud_color = graphics.Color(140, 140, 160)
hail_color = graphics.Color(200, 220, 255)
_draw_cloud_shape(canvas, x + 4, y + 4, cloud_color)
for i in range(3):
hx = x + 8 + i * 5
hy = y + 18 + (i % 2) * 2
graphics.DrawCircle(canvas, hx, hy, 2, hail_color)
def draw_unknown(canvas, x, y):
"""Draw question mark for unknown weather."""
color = graphics.Color(255, 255, 255)
graphics.DrawCircle(canvas, x + 12, y + 8, 4, color)
canvas.SetPixel(x + 12, y + 14, 255, 255, 255)
canvas.SetPixel(x + 12, y + 17, 255, 255, 255)
# Weather state to icon function mapping
WEATHER_ICONS = {
'sunny': draw_sunny,
'clear-day': draw_sunny,
'clear-night': draw_clear_night,
'partlycloudy': draw_partly_cloudy,
'partly-cloudy-day': draw_partly_cloudy,
'partly-cloudy-night': draw_partly_cloudy_night,
'cloudy': draw_cloudy,
'overcast': draw_cloudy,
'rainy': draw_rainy,
'rain': draw_rainy,
'showers': draw_rainy,
'pouring': draw_pouring,
'snowy': draw_snowy,
'snow': draw_snowy,
'snowy-rainy': draw_snowy,
'sleet': draw_snowy,
'hail': draw_hail,
'lightning': draw_thunderstorm,
'lightning-rainy': draw_thunderstorm,
'thunderstorm': draw_thunderstorm,
'fog': draw_foggy,
'foggy': draw_foggy,
'mist': draw_foggy,
'hazy': draw_foggy,
'windy': draw_windy,
'windy-variant': draw_windy,
'exceptional': draw_unknown,
}
def draw_weather_icon(canvas, x, y, weather_state):
"""Draw weather icon based on state string."""
weather_lower = weather_state.lower() if weather_state else ''
# Try exact match
if weather_lower in WEATHER_ICONS:
WEATHER_ICONS[weather_lower](canvas, x, y)
return
# Try partial match
for key, draw_func in WEATHER_ICONS.items():
if key in weather_lower or weather_lower in key:
draw_func(canvas, x, y)
return
draw_unknown(canvas, x, y)