Raspi_homeassitant

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.

Replace this 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

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
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,
)

_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]),
        ]
        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."""

        with open(self._config[CONF_FILE_PATH], "rb") as file:
            original_image = Image.open(file)

            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))

            # Add timestamp
            draw = ImageDraw.Draw(cropped_image)
            text_color = (255, 255, 255)
            text_position = (10, 10)
            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

    @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.

Categories:

Tags:

Comments are closed