Since an update on my Raspberry Pi 4 to Debian Bookworm the internal RaspiCam isn’t working anymore. The Homeassistant script tries to call raspistill, which doesn’t exist and doesn’t work anymore on bullseye. But there is an alternative! The new tool is rpicam-still, which is not as powerful but it works. If you made the same decision to update to bullseye and still want to use your camera, than you can use this script as a workaround. It’s still not working like the old but you will have an image.
First check if in /boot/config.txt is camera_auto_detect=1 set.
Then replace the camera.py file. It can be anywhere, so maybe search for it and edit it as follows:
find / -type f -name "camera.py" 2>/dev/null
/srv/homeassistant/lib/python3.11/site-packages/homeassistant/components/rpi_camera/camera.py
Edit the file (check if it is the rpi_camera-folder) and replace it with this code:
"""Camera platform that has a Raspberry Pi camera."""
from __future__ import annotations
import logging
import os
import shutil
import subprocess
from tempfile import NamedTemporaryFile
from PIL import Image, ImageDraw, ImageFont, ImageStat
from datetime import datetime
from io import BytesIO
from homeassistant.components.camera import Camera
from homeassistant.const import CONF_FILE_PATH, CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import (
CONF_HORIZONTAL_FLIP,
CONF_IMAGE_HEIGHT,
CONF_IMAGE_QUALITY,
CONF_IMAGE_ROTATION,
CONF_IMAGE_WIDTH,
CONF_OVERLAY_METADATA,
CONF_OVERLAY_TIMESTAMP,
CONF_TIMELAPSE,
CONF_VERTICAL_FLIP,
DOMAIN,
)
TEXT_X = 10
TEXT_Y = 10
_LOGGER = logging.getLogger(__name__)
def kill_rpicam_still(*args):
"""Kill any previously running rpicam-still process.."""
with subprocess.Popen(
["killall", "rpicam-still"],
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
close_fds=False, # required for posix_spawn
):
pass
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Raspberry Camera."""
# We only want this platform to be set up via discovery.
# prevent initializing by erroneous platform config section in yaml conf
if discovery_info is None:
return
if shutil.which("rpicam-still") is None:
_LOGGER.error("'rpicam-still' was not found")
return
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, kill_rpicam_still)
setup_config = hass.data[DOMAIN]
file_path = setup_config[CONF_FILE_PATH]
def delete_temp_file(*args):
"""Delete the temporary file to prevent saving multiple temp images.
Only used when no path is defined
"""
os.remove(file_path)
# If no file path is defined, use a temporary file
if file_path is None:
with NamedTemporaryFile(suffix=".jpg", delete=False) as temp_file:
file_path = temp_file.name
setup_config[CONF_FILE_PATH] = file_path
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, delete_temp_file)
# Check whether the file path has been whitelisted
elif not hass.config.is_allowed_path(file_path):
_LOGGER.error("'%s' is not a whitelisted directory", file_path)
return
add_entities([RaspberryCamera(setup_config)])
class RaspberryCamera(Camera):
"""Representation of a Raspberry Pi camera."""
def __init__(self, device_info):
"""Initialize Raspberry Pi camera component."""
super().__init__()
self._name = device_info[CONF_NAME]
self._config = device_info
# Kill if there's rpicam-still instance
kill_rpicam_still()
cmd_args = [
"rpicam-still",
"--nopreview",
"-o",
device_info[CONF_FILE_PATH],
"-t",
"0",
"--width",
str(device_info[CONF_IMAGE_WIDTH]),
"--height",
str(device_info[CONF_IMAGE_HEIGHT]),
"--timelaps",
str(device_info[CONF_TIMELAPSE]),
"-q",
str(device_info[CONF_IMAGE_QUALITY]),
# "--rotation",
# str(device_info[CONF_IMAGE_ROTATION]),
]
if device_info[CONF_HORIZONTAL_FLIP]:
cmd_args.append("--hflip")
if device_info[CONF_VERTICAL_FLIP]:
cmd_args.append("--vflip")
# if device_info[CONF_OVERLAY_METADATA]:
# cmd_args.append("--info-text")
# cmd_args.append(str(device_info[CONF_OVERLAY_METADATA]))
#
# if device_info[CONF_OVERLAY_TIMESTAMP]:
# cmd_args.append("--info-text")
# cmd_args.append("4")
# cmd_args.append("--info-text")
# cmd_args.append(str(device_info[CONF_OVERLAY_TIMESTAMP]))
# The rpicam-still process started below must run "forever" in
# the background until killed when Home Assistant is stopped.
# Therefore it must not be wrapped with "with", since that
# waits for the subprocess to exit before continuing.
subprocess.Popen( # pylint: disable=consider-using-with
cmd_args,
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
close_fds=False, # required for posix_spawn
)
def camera_image(self, width: int | None = None, height: int | None = None) -> bytes | None:
"""Return rpicam-still image response."""
try:
with open(self._config[CONF_FILE_PATH], "rb") as file:
try:
original_image = Image.open(file)
except:
return None
angle_to_rotate = self._config[CONF_IMAGE_ROTATION]
rotated_image = original_image.rotate(angle_to_rotate)
# Get the width and height of the original image
width, height = original_image.size
# Calculate the crop dimensions for 16:9 aspect ratio
crop_width = min(rotated_image.size[0], rotated_image.size[1] * 16 / 9)
crop_height = crop_width * 9 / 16
# Crop the image to 16:9 aspect ratio
left = (rotated_image.size[0] - crop_width) / 2
top = (rotated_image.size[1] - crop_height) / 2
right = (rotated_image.size[0] + crop_width) / 2
bottom = (rotated_image.size[1] + crop_height) / 2
cropped_image = rotated_image.crop((left, top, right, bottom))
# Analyze the top-left corner color for text contrast adjustment
# Here we take a 10x10 pixel sample from the top left corner
sample_area = cropped_image.crop((TEXT_X, TEXT_Y, TEXT_X+40, TEXT_Y+10))
stat = ImageStat.Stat(sample_area)
avg_color = stat.mean[:3] # Get the average RGB values
# Determine if the average color is light or dark
brightness = sum(avg_color) / 3 # Simple brightness estimation
text_color = (0, 0, 0) if brightness > 128 else (255, 255, 255) # Dark text if light background, else white
# Add timestamp
draw = ImageDraw.Draw(cropped_image)
text_position = (TEXT_X, TEXT_Y)
font_size = int(width * 0.03)
font = ImageFont.load_default(size=font_size)
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
draw.text(text_position, current_time, font=font, fill=text_color)
# Save the rotated and cropped image
img_bytes_io = BytesIO()
cropped_image.save(img_bytes_io, format='JPEG')
img_bytes = img_bytes_io.getvalue()
return img_bytes
except FileNotFoundError:
return None
print(f"Error: The file at {self._config[CONF_FILE_PATH]} was not found.")
# You can add additional error handling logic here if needed
except Exception as e:
return None
print(f"An unexpected error occurred: {e}")
@property
def name(self):
"""Return the name of this camera."""
return self._name
@property
def frame_interval(self):
"""Return the interval between frames of the stream."""
return self._config[CONF_TIMELAPSE] / 1000
Feel free to modify the code. It adds a timestamp to the top left corner.

Comments are closed