TheRealWaldo / thermal

Thermal Vision Sensor and Camera for Home Assistant
Apache License 2.0
65 stars 5 forks source link

camera entities no longer available after HA update #169

Closed ThreeFN closed 1 year ago

ThreeFN commented 1 year ago

At some point in the not too distant parts (2022.10 or 2022.9) after a HA update my camera entities became no longer available. Unfortunately I don't have much to go on in order to try and debug this problem myself.

In logs, I'm seeing some errors related to camera.py, 'extra_state_attributes':

2022-10-13 17:07:08.372 WARNING (SyncWorker_0) [homeassistant.loader] We found a custom integration thermal_vision which has not been tested by Home Assistant. This component might cause stability problems, be sure to disable it if you experience issues with Home Assistant
2022-10-13 17:07:21.531 ERROR (MainThread) [homeassistant.components.camera] Error adding entities for domain camera with platform thermal_vision
File "/config/custom_components/thermal_vision/camera.py", line 212, in extra_state_attributes
2022-10-13 17:07:21.582 ERROR (MainThread) [homeassistant.components.camera] Error while setting up thermal_vision platform for camera
File "/config/custom_components/thermal_vision/camera.py", line 212, in extra_state_attributes
2022-10-13 17:07:31.279 ERROR (MainThread) [homeassistant.components.homekit] HomeKit M4 Thermal Camera cannot startup: entity not available: {'include_domains': [], 'exclude_domains': [], 'include_entities': ['camera.m4_thermal_camera'], 'exclude_entities': [], 'include_entity_globs': [], 'exclude_entity_globs': []}
2022-10-13 17:07:31.408 ERROR (MainThread) [homeassistant.components.homekit] HomeKit Mini Cooper Thermal Camera cannot startup: entity not available: {'include_domains': [], 'exclude_domains': [], 'include_entities': ['camera.mini_cooper_thermal_camera'], 'exclude_entities': [], 'include_entity_globs': [], 'exclude_entity_globs': []}
File "/config/custom_components/thermal_vision/camera.py", line 212, in extra_state_attributes
File "/config/custom_components/thermal_vision/camera.py", line 212, in extra_state_attributes
File "/config/custom_components/thermal_vision/camera.py", line 212, in extra_state_attributes
File "/config/custom_components/thermal_vision/camera.py", line 212, in extra_state_attributes
File "/config/custom_components/thermal_vision/camera.py", line 212, in extra_state_attributes

configuration:

  - platform: thermal_vision
    name: Mini Cooper Thermal Camera
    #overlay: true
    pixel_sensor: sensor.mini_cooper_car_sensor_pixels
    #    # rotate: 180
    #auto_range: true
    interpolate:
      method: disabled

I'm using ESPHome custom devices (ultrasonic/temp sensors in addition to the AMG8833 sensor) so I can't debug that ESPHome isn't the culprit using the ESP-only firmware, but all the sensor values/etc in HA seem to be working fine, so I doubt it's ESPHome's fault. If ESPHome is to blame the only thing I can think is that there is some change to the text-sensor sensor value that the thermal-vision parser isn't interpreting correctly.

esphome, FYi this is a packaged config, since I have multiple of these sensors:

esphome:
  name: $devicename
  platform: ESP32
  board: tinypico
  includes:
  - custom/amg8833.h
  - custom/amg8833_camera.h
  libraries:
    - "Wire"
    - "SparkFun GridEYE AMG88 Library"

  on_boot:
    then:
      - output.turn_on: gpio_led
      - light.control:
          id: ledneo
          brightness: 0.25
          state: on
          green: 1
          red: 0.5

packages:
  base: !include base.yaml

i2c:
  sda: 21
  scl: 22
  scan: true
  frequency: 400kHz

bme680_bsec:
  address: 0x77

output:
  - platform: gpio
    pin: 13
    inverted: true
    id: gpio_led

light:
  - platform: fastled_spi
    chipset: APA102
    data_pin: 2
    clock_pin: 12
    num_leds: 1
    rgb_order: BGR
    id: ledneo

interval:
  - interval: 2s
    then:
      if:
        condition:
          wifi.connected:
        then:
          - light.control:
              id: ledneo
              blue: 1
              green: 0
        else:
          - light.control:
              id: ledneo
              blue: 0
              green: 1

text_sensor:

  - platform: bme680_bsec
    iaq_accuracy:
      name: "${friendly_name} BME680 IAQ Accuracy"

  - platform: custom
    lambda: |-
      auto amg8833 = new AMG8833CameraComponent();
      App.register_component(amg8833);
      return {amg8833};
    text_sensors:
      - name: "${friendly_name} Pixels"

binary_sensor:
  - platform: template
    name: "${friendly_name} Present"
    lambda: |-
      if (id(ultradistance).state < ${vehicle_height}) {
        //if sensor less than trigger height, car is present
        return true;
      } else {
        //car is not present
        return false;
      }

sensor:

  - platform: bme680_bsec
    temperature:
      name: "${friendly_name} BME680 Temperature"
      id: bme680temperature
    pressure:
      name: "${friendly_name} BME680 Pressure"
    humidity:
      name: "${friendly_name} BME680 Humidity"
      id: bme680humidity
    iaq:
      name: "${friendly_name} BME680 IAQ"
    co2_equivalent:
      name: "${friendly_name} BME680 eCO2"
    breath_voc_equivalent:
      name: "${friendly_name} BME680 Breath eVOC"

  - platform: ultrasonic
    trigger_pin: 33
    echo_pin: 32
    name: "${friendly_name} Ultrasonic Sensor"
    id: ultradistance
    update_interval: 5s
    timeout: 3m
    filters:
      - median:
          window_size: 11
          send_every: 6
          send_first_at: 5

  - platform: custom
    lambda: |-
      auto amg8833 = new AMG8833Component();
      App.register_component(amg8833);
      return {amg8833->sensor_temperature, amg8833->max_temperature, amg8833->min_temperature, amg8833->avg_temperature, amg8833->min_index, amg8833->max_index};

    sensors:
      - name: "${friendly_name} Thermal Sensor Temperature"
        unit_of_measurement: °C
        device_class: temperature
        accuracy_decimals: 2

      - name: "${friendly_name} Thermal Sensor Max"
        id: tmax
        unit_of_measurement: °C
        device_class: temperature
        accuracy_decimals: 2

      - name: "${friendly_name} Thermal Sensor Min"
        id: tmin
        unit_of_measurement: °C
        device_class: temperature
        accuracy_decimals: 2

      - name: "${friendly_name} Thermal Sensor Avg"
        id: tavg
        unit_of_measurement: °C
        device_class: temperature
        accuracy_decimals: 2

      - name: "${friendly_name} Thermal Sensor Min Index"
        accuracy_decimals: 0

      - name: "${friendly_name} Thermal Sensor Max Index"
        accuracy_decimals: 0
ThreeFN commented 1 year ago

Shot of caffeine or something removed the cob webs, found this: https://developers.home-assistant.io/blog/2022/09/28/deprecate-conversion-utilities/

Going to see what I can figure out/fix, etc.

ThreeFN commented 1 year ago

Managed to butcher something into working.

Main things seemed to be adding: from homeassistant.util.unit_conversion import TemperatureConverter and: TEMP_FAHRENHEIT, and changing the unit conversion while I was at it, eg: else TemperatureConverter.convert(self._pixel_min_temp,TEMP_CELSIUS,TEMP_FAHRENHEIT),

I'm mostly syntactically blind/inept, this is not comment/etc complete, but someone that codes more professionally should be able to clean this up a fair bit and push a commit, it seems to be working for me:

thermal_vision/camera.py

"""Thermal Vision Camera"""

import logging
import asyncio
import aiohttp
import async_timeout

import io
import time
import base64

import numpy as np
import voluptuous as vol

from colour import Color
from PIL import Image, ImageDraw

from homeassistant import util
from homeassistant.util.unit_conversion import TemperatureConverter
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.helpers import config_validation as cv

from homeassistant.const import (
    CONF_HOST,
    CONF_NAME,
    CONF_VERIFY_SSL,
    TEMP_CELSIUS,
    TEMP_FAHRENHEIT,
    STATE_UNKNOWN,
    STATE_UNAVAILABLE,
)

from .utils import constrain, map_value
from .interpolate import interpolate
from urllib.parse import urljoin
from .client import ThermalVisionClient

from .const import (
    CONF_OVERLAY,
    CONF_SESSION_TIMEOUT,
    DEFAULT_OVERLAY,
    DEFAULT_SESSION_TIMEOUT,
    CONF_WIDTH,
    CONF_HEIGHT,
    CONF_PRESERVE_ASPECT_RATIO,
    CONF_METHOD,
    CONF_AUTO_RANGE,
    CONF_MIN_DIFFERANCE,
    CONF_MIN_TEMPERATURE,
    CONF_MAX_TEMPERATURE,
    CONF_ROTATE,
    CONF_MIRROR,
    CONF_FORMAT,
    CONF_COLD_COLOR,
    CONF_HOT_COLOR,
    CONF_SENSOR,
    CONF_INTERPOLATE,
    CONF_ROWS,
    CONF_COLS,
    CONF_PIXEL_SENSOR,
    DEFAULT_NAME,
    DEFAULT_VERIFY_SSL,
    DEFAULT_IMAGE_WIDTH,
    DEFAULT_IMAGE_HEIGHT,
    DEFAULT_PRESERVE_ASPECT_RATIO,
    DEFAULT_METHOD,
    DEFAULT_MIN_TEMPERATURE,
    DEFAULT_MAX_TEMPERATURE,
    DEFAULT_ROTATE,
    DEFAULT_MIRROR,
    DEFAULT_FORMAT,
    DEFAULT_ROWS,
    DEFAULT_COLS,
    DEFAULT_COLD_COLOR,
    DEFAULT_HOT_COLOR,
)

_LOGGER = logging.getLogger(__name__)

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
    {
        vol.Exclusive(CONF_HOST, 1): cv.url,
        vol.Exclusive(CONF_PIXEL_SENSOR, 1): cv.entity_id,
        vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
        vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
        vol.Optional(CONF_WIDTH, default=DEFAULT_IMAGE_WIDTH): cv.positive_int,
        vol.Optional(CONF_HEIGHT, default=DEFAULT_IMAGE_HEIGHT): cv.positive_int,
        vol.Optional(
            CONF_PRESERVE_ASPECT_RATIO, default=DEFAULT_PRESERVE_ASPECT_RATIO
        ): cv.boolean,
        vol.Optional(CONF_AUTO_RANGE, default=False): cv.boolean,
        vol.Optional(CONF_MIN_DIFFERANCE, default=4): cv.positive_int,
        vol.Optional(CONF_MIN_TEMPERATURE, default=DEFAULT_MIN_TEMPERATURE): vol.All(
            vol.Coerce(float), vol.Range(min=0, max=100), msg="invalid min temperature"
        ),
        vol.Optional(CONF_MAX_TEMPERATURE, default=DEFAULT_MAX_TEMPERATURE): vol.All(
            vol.Coerce(float), vol.Range(min=0, max=100), msg="invalid max temperature"
        ),
        vol.Optional(CONF_SENSOR): vol.Schema(
            {
                vol.Required(CONF_ROWS): cv.positive_int,
                vol.Required(CONF_COLS): cv.positive_int,
            }
        ),
        vol.Optional(CONF_INTERPOLATE): vol.Schema(
            {
                vol.Optional(CONF_ROWS): cv.positive_int,
                vol.Optional(CONF_COLS): cv.positive_int,
                vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): cv.string,
            }
        ),
        vol.Optional(CONF_FORMAT, default=DEFAULT_FORMAT): cv.string,
        vol.Optional(CONF_MIRROR, default=DEFAULT_MIRROR): cv.boolean,
        vol.Optional(CONF_ROTATE, default=DEFAULT_ROTATE): cv.positive_int,
        vol.Optional(CONF_COLD_COLOR, default=DEFAULT_COLD_COLOR): cv.string,
        vol.Optional(CONF_HOT_COLOR, default=DEFAULT_HOT_COLOR): cv.string,
        vol.Optional(
            CONF_SESSION_TIMEOUT, default=DEFAULT_SESSION_TIMEOUT
        ): cv.positive_int,
        vol.Optional(CONF_OVERLAY, default=DEFAULT_OVERLAY): cv.boolean,
    },
    {
        vol.Required(vol.Any(CONF_HOST, CONF_PIXEL_SENSOR)),
    },
)

async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
    """Set up camera component."""
    _LOGGER.debug("async setup Thermal camera")
    async_add_entities([ThermalVisionCamera(config, hass)])

class ThermalVisionCamera(Camera):
    """A camera component producing thermal image from grid sensor data"""

    def __init__(self, config, hass):
        """Initialize the component."""
        super().__init__()
        self._name = config.get(CONF_NAME)
        _LOGGER.debug(f"Initialize Thermal camera {self._name}")

        self._host = config.get(CONF_HOST)
        self._pixel_sensor = config.get(CONF_PIXEL_SENSOR)

        self._image_width = config.get(CONF_WIDTH)
        self._image_height = config.get(CONF_HEIGHT)
        self._preserve_aspect_ratio = config.get(CONF_PRESERVE_ASPECT_RATIO)
        self._min_temperature = config.get(CONF_MIN_TEMPERATURE)
        self._max_temperature = config.get(CONF_MAX_TEMPERATURE)
        self._pixel_min_temp = self._min_temperature
        self._pixel_max_temp = self._min_temperature
        self._color_depth = 1024
        self._rotate = config.get(CONF_ROTATE)
        self._mirror = config.get(CONF_MIRROR)
        self._format = config.get(CONF_FORMAT)
        self._session_timeout = config.get(CONF_SESSION_TIMEOUT)
        self._overlay = config.get(CONF_OVERLAY)
        self._min_diff = config.get(CONF_MIN_DIFFERANCE)
        self._fps = None
        self._temperature_unit = hass.config.units.temperature_unit
        _LOGGER.debug("Temperature unit %s", self._temperature_unit)

        sensor = config.get(
            CONF_SENSOR, {CONF_ROWS: DEFAULT_ROWS, CONF_COLS: DEFAULT_COLS}
        )
        self._rows = sensor.get(CONF_ROWS, DEFAULT_ROWS)
        self._cols = sensor.get(CONF_COLS, DEFAULT_COLS)

        interpolate = config.get(
            CONF_INTERPOLATE,
            {CONF_ROWS: 32, CONF_COLS: 32, CONF_METHOD: DEFAULT_METHOD},
        )
        self._interpolate_rows = interpolate.get(CONF_ROWS, 32)
        self._interpolate_cols = interpolate.get(CONF_COLS, 32)
        self._method = interpolate.get(CONF_METHOD, DEFAULT_METHOD)

        self._auto_range = config.get(CONF_AUTO_RANGE)
        self._verify_ssl = config.get(CONF_VERIFY_SSL)

        color_cold = config.get(CONF_COLD_COLOR)
        color_hot = config.get(CONF_HOT_COLOR)
        self._colors = list(
            Color(color_cold).range_to(Color(color_hot), self._color_depth)
        )
        self._colors = [
            (int(c.red * 255), int(c.green * 255), int(c.blue * 255))
            for c in self._colors
        ]
        if self._host:
            self._client = ThermalVisionClient(self._host, self._verify_ssl)

        self._setup_default_image()

    @property
    def name(self):
        """Return the component name."""
        return self._name

    @property
    def should_poll(self):
        """Need to poll for attributes."""
        return True

    # @property
    # def extra_state_attributes(self):
    #     """Return the camera state attributes."""
    #     return {
    #         "fps": self._fps,
    #         "min": self._pixel_min_temp
    #         if self._temperature_unit == TEMP_CELSIUS
    #         else util.temperature.celsius_to_fahrenheit(self._pixel_min_temp),
    #         "max": self._pixel_max_temp
    #         if self._temperature_unit == TEMP_CELSIUS
    #         else util.temperature.celsius_to_fahrenheit(self._pixel_max_temp),
    #         "range_min": self._min_temperature
    #         if self._temperature_unit == TEMP_CELSIUS
    #         else util.temperature.celsius_to_fahrenheit(self._min_temperature),
    #         "range_max": self._max_temperature
    #         if self._temperature_unit == TEMP_CELSIUS
    #         else util.temperature.celsius_to_fahrenheit(self._max_temperature),
    #     }
    @property
    def extra_state_attributes(self):
        """Return the camera state attributes."""
        return {
            "fps": self._fps,
            "min": self._pixel_min_temp
            if self._temperature_unit == TEMP_CELSIUS
            else TemperatureConverter.convert(self._pixel_min_temp,TEMP_CELSIUS,TEMP_FAHRENHEIT),
            "max": self._pixel_max_temp
            if self._temperature_unit == TEMP_CELSIUS
            else TemperatureConverter.convert(self._pixel_max_temp,TEMP_CELSIUS,TEMP_FAHRENHEIT),
            "range_min": self._min_temperature
            if self._temperature_unit == TEMP_CELSIUS
            else TemperatureConverter.convert(self._min_temperature,TEMP_CELSIUS,TEMP_FAHRENHEIT),
            "range_max": self._max_temperature
            if self._temperature_unit == TEMP_CELSIUS
            else TemperatureConverter.convert(self._max_temperature,TEMP_CELSIUS,TEMP_FAHRENHEIT),
        }

    async def async_camera_image(self, width=None, height=None):
        """Pull image from camera"""
        self._set_size(width, height)
        if self._host:
            start = int(round(time.time() * 1000))
            websession = async_get_clientsession(self.hass, verify_ssl=self._verify_ssl)
            try:
                with async_timeout.timeout(self._session_timeout):
                    response = await websession.get(urljoin(self._host, "raw"))
                    jsonResponse = await response.json()
                    if jsonResponse:
                        data = jsonResponse["data"].split(",")
                        self._setup_range(data)
                        self._default_image = self._camera_image(data)

            except asyncio.TimeoutError:
                _LOGGER.warning("Timeout getting camera image from %s", self._name)
                return self._default_image

            except aiohttp.ClientError as err:
                _LOGGER.error(
                    "Error getting new camera image from %s: %s", self._name, err
                )
                return self._default_image

            except Exception as err:
                _LOGGER.error("Failed to generate camera (%s)", err)
                return self._default_image

            self._fps = int(1000.0 / (int(round(time.time() * 1000)) - start))
        else:
            self._update_pixel_sensor()

        return self._default_image

    def camera_image(self, width=None, height=None):
        """Get image for camera"""
        self._set_size(width, height)

        if self._host:
            self._client.call()
            return self._camera_image(self._client.get_raw())
        else:
            self._update_pixel_sensor()
            return self._default_image

    def _set_size(self, width=None, height=None):
        """Set output image size"""
        if width:
            self._image_width = width

        if self._preserve_aspect_ratio and width:
            self._image_height = int(width * (self._rows / self._cols))
        else:
            if height:
                self._image_height = height

    def _update_pixel_sensor(self):
        """Decode pixels from sensor and update camera image"""

        encoded_pixels = self.hass.states.get(self._pixel_sensor).state
        _LOGGER.debug("Decoding pixels: %s", encoded_pixels)
        if encoded_pixels not in [STATE_UNKNOWN, STATE_UNAVAILABLE]:
            data = []
            for char in base64.b64decode(encoded_pixels):
                if char & (1 << 11):
                    char &= ~(1 << 11)
                    char = char * -1
                data.append(char * 0.25)
            self._setup_range(data)
            self._default_image = self._camera_image(data)

    def _setup_range(self, pixels):
        """Perform auto-ranging"""
        self._pixel_min_temp = float(min(pixels))
        self._pixel_max_temp = float(max(pixels))
        if self._auto_range:
            _LOGGER.debug("Minimum temperature %s", self._pixel_min_temp)
            _LOGGER.debug("Maximum temperature %s", self._pixel_max_temp)
            if (
                self._pixel_min_temp != self._pixel_max_temp
                and self._pixel_max_temp > self._pixel_min_temp
                and not (
                    self._pixel_min_temp == self._min_temperature
                    and self._pixel_max_temp == self._max_temperature
                )
            ):
                self._min_temperature = self._pixel_min_temp
                self._max_temperature = self._pixel_max_temp
                diff = self._max_temperature - self._min_temperature

                if diff < self._min_diff:
                    self._max_temperature = self._min_temperature + self._min_diff

    def _setup_default_image(self):
        """Set up a default image"""
        self._default_image = self._camera_image(
            np.full(self._rows * self._cols, self._min_temperature)
        )

    def _camera_image(self, pixels):
        """Create image from thermal camera pixels (temperatures)"""
        # Map to colors depth range
        pixels = [
            map_value(
                p,
                self._min_temperature,
                self._max_temperature,
                0,
                self._color_depth - 1,
            )
            for p in pixels
        ]
        # Convert to 2D
        pixels = np.reshape(pixels, (self._rows, self._cols))
        # Rotate (flip)
        if self._rotate == 180:
            pixels = np.flip(pixels, 0)
        # Mirror
        if self._mirror:
            pixels = np.flip(pixels, 1)

        if self._method != "disabled":
            # Input / output grid
            xi = np.linspace(0, self._cols - 1, self._cols)
            yi = np.linspace(0, self._rows - 1, self._rows)
            xo = np.linspace(0, self._cols - 1, self._interpolate_cols)
            yo = np.linspace(0, self._rows - 1, self._interpolate_rows)
            # Interpolate
            interpolation = interpolate(xi, yi, pixels, xo, yo, self._method)
            # Draw surface
            image = Image.new("RGB", (self._image_width, self._image_height))
            draw = ImageDraw.Draw(image)
            # Pixel size
            pixel_width = self._image_width / self._interpolate_cols
            pixel_height = self._image_height / self._interpolate_rows
            # Draw intepolated image
            for y, row in enumerate(interpolation):
                for x, pixel in enumerate(row):
                    color_index = constrain(int(pixel), 0, self._color_depth - 1)
                    x0 = pixel_width * x
                    y0 = pixel_height * y
                    x1 = x0 + pixel_width
                    y1 = y0 + pixel_height
                    draw.rectangle(((x0, y0), (x1, y1)), fill=self._colors[color_index])
        else:
            image = Image.new("RGB", (self._image_width, self._image_height))
            draw = ImageDraw.Draw(image)
            pixel_width = self._image_width / self._cols
            pixel_height = self._image_height / self._rows
            for y, row in enumerate(pixels):
                for x, pixel in enumerate(row):
                    color_index = constrain(int(pixel), 0, self._color_depth - 1)
                    x0 = pixel_width * x
                    y0 = pixel_height * y
                    x1 = x0 + pixel_width
                    y1 = y0 + pixel_height
                    draw.rectangle(((x0, y0), (x1, y1)), fill=self._colors[color_index])

        # Add overlay
        # if self._overlay:
        #     min_temp = (
        #         self._pixel_min_temp
        #         if self._temperature_unit == TEMP_CELSIUS
        #         else util.temperature.celsius_to_fahrenheit(self._pixel_min_temp)
        #     )
        #     max_temp = (
        #         self._pixel_max_temp
        #         if self._temperature_unit == TEMP_CELSIUS
        #         else util.temperature.celsius_to_fahrenheit(self._pixel_max_temp)
        #     )
        #     min_temperature = (
        #         self._min_temperature
        #         if self._temperature_unit == TEMP_CELSIUS
        #         else util.temperature.celsius_to_fahrenheit(self._min_temperature)
        #     )
        #     max_temperature = (
        #         self._max_temperature
        #         if self._temperature_unit == TEMP_CELSIUS
        #         else util.temperature.celsius_to_fahrenheit(self._max_temperature)
        #     )
        # Add overlay
        if self._overlay:
            min_temp = (
                self._pixel_min_temp
                if self._temperature_unit == TEMP_CELSIUS
                else TemperatureConverter.convert(self._pixel_min_temp,TEMP_CELSIUS,TEMP_FAHRENHEIT)
            )
            max_temp = (
                self._pixel_max_temp
                if self._temperature_unit == TEMP_CELSIUS
                else TemperatureConverter.convert(self._pixel_max_temp,TEMP_CELSIUS,TEMP_FAHRENHEIT)
            )
            min_temperature = (
                self._min_temperature
                if self._temperature_unit == TEMP_CELSIUS
                else TemperatureConverter.convert(self._min_temperature,TEMP_CELSIUS,TEMP_FAHRENHEIT)
            )
            max_temperature = (
                self._max_temperature
                if self._temperature_unit == TEMP_CELSIUS
                else TemperatureConverter.convert(self._max_temperature,TEMP_CELSIUS,TEMP_FAHRENHEIT)
            )

            draw.multiline_text(
                (10, 10),
                f"Min: {min_temp}{self._temperature_unit}\nMax: {max_temp}{self._temperature_unit}\nRange: {min_temperature}{self._temperature_unit} - {max_temperature}{self._temperature_unit}",
                fill=(255, 255, 0),
            )

        # Return image
        with io.BytesIO() as output:
            if self._format == "jpeg":
                image.save(
                    output,
                    format=self._format,
                    quality=80,
                    optimize=True,
                    progressive=True,
                )
            else:
                image.save(output, format=self._format)
            return output.getvalue()
TheRealWaldo commented 1 year ago

Interesting; everything is still working for me. Is your system set to Fahrenheit?

ThreeFN commented 1 year ago

Unfortunately yes, my system is set to Fahrenheit here in the US.

All this use to work just fine, an update was definitely the culprit, so I'm at just as much of a loss why TEMP_FAHRENHEIT needs to be defined for the if self._temperature_unit == TEMP_CELSIUS logic test, it should just return false, but I know next to nothing about python and how type specific it is.

ThreeFN commented 1 year ago

Now that I look at, I'm actually thinking that it should look something more like this, as it could help handle TEMP_KELVIN and I forgot that I had hard coded the TEMP_CELSIUS to the TEMP_FAHRENHEIT conversion, which is poor form.

from homeassistant.const import (
    CONF_HOST,
    CONF_NAME,
    CONF_VERIFY_SSL,
    TEMP_CELSIUS,
    TEMP_FAHRENHEIT,
    TEMP_KELVIN,
    STATE_UNKNOWN,
    STATE_UNAVAILABLE,
)
 "min": self._pixel_min_temp
            if self._temperature_unit == TEMP_CELSIUS
            else TemperatureConverter.convert(self._pixel_min_temp,TEMP_CELSIUS,self._temperature_unit),
TheRealWaldo commented 1 year ago

I think someone also recently changed how conversion entities work, where they moved it more to the core instead of having to do the conversion in every single sensor that uses it.

This deprecation may just be a way of enforcing the knowledge of the new changes without calling it out explicitly.

Might not need to do any conversion whatsoever.

I'm not seeing a problem because I use Celsius...

Will do some more research over the weekend and see if I can get a new version out next week.

ThreeFN commented 1 year ago

I think someone also recently changed how conversion entities work, where they moved it more to the core instead of having to do the conversion in every single sensor that uses it.

This deprecation may just be a way of enforcing the knowledge of the new changes without calling it out explicitly.

Might not need to do any conversion whatsoever.

I'm not seeing a problem because I use Celsius...

Will do some more research over the weekend and see if I can get a new version out next week.

Much appreciated sir.

I'm still perplexed how the logic check could fail when TEMP_FAHRENHEIT isn't defined.

The util.temperature.celsius_to_fahrenheit deprecation shouldn't be enforced yet, I think it said 2023.4 was planned sunset, but I think I remember reading a release note saying that they accidently put in 2022.4 by mistake, not sure that change was pushed to current or not. Sorry I was trying alot of different things and researching alot of different things, and I wasn't taking proper dev notes.

I think I remember fixing just the self._temperature_unit == TEMP_CELSIUS logic check by adding TEMP_FAHRENHEIT to the definitions and there was still an error reported about the next line with the util.temperature.celsius_to_fahrenheit conversion, which would indicate that deprecation was 'enforced,' intentional or otherwise.

I'd agree that I would think that the conversion is handled more internally to HA, and that a sensor can simply be presented to HA as a CELSIUS sensor and let it take care of the rest.

The only gotcha would be the temperature overlay block of code, that may need to continue to interrogate the config file and do conversions using the replacement TemperatureConverter.convert method.

briangunderson commented 1 year ago

I hadn't seen this github issue when I decided to troubleshoot this myself. FWIW I just changed line 162 of camera.py to self._temperature_unit = TEMP_CELSIUS and made sure I wasn't using the overlay option. It's a hack but it more or less validates the fact that it is the temperature conversion that is causing the camera entity to fail to load. Cheers!

TheRealWaldo commented 1 year ago

I am closing this as I've pushed out a new version that should fix this. Let me know if there are still issues!

ThreeFN commented 1 year ago

I am closing this as I've pushed out a new version that should fix this. Let me know if there are still issues!

Solved it for me as best I can tell, camera entities show up fine.

thank you kindly.