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