Open doenau opened 1 year ago
Honestly, this is something the whole "HA" world is missing...partially I can understand why (the majority of entities are exposed by integrations that should take care of it) but mostly, imho, it seems a de-facto choice in the HA world.
Anyhow...the only way to mitigate this issue is to use an old script, which still works fortunately: https://github.com/xannor/hass_py_set_state
(there is also a more recent version here but I'm unsure about it)
Please note, despite what stated in the readme you can not install it trough HACS: you need to manually create the phyton_scripts
folder in "config" folder of your HA, and then you need to add the python_script:
in your configuration.yaml
. Reload your HA instance and follow the examples on the readme for how to call/set the service.
disclaimer: I was 20 years ago a coder, only a biz guy now. so bare with me. Problem: my HQ garage use this. What we need is to now at that moment if the car is authorized to enter, the plate itself is useless. So I added an attribute allowed_vehicle_detected that is resetted each time the integration runs. If any plate authorized is recognized is turn on , otherwise if off. Now I just added a sensor reading allowed_vehicle_detected and added a binary input OpenGate. Created an automation that is fired everytime plate recongnition runs. If the plate ( allowed_vehicle_detected = true) then OpenGate goes ON, fire the dry contact at the garage door, waits 5 secs and OpenGate goes OFF again.
A much more elegant solution would be to have a method of resetting the watched_plates status, either manually (via an action we could fire, or via timer)
"""Vehicle detection using Plate Recognizer cloud service.""" import logging import requests import voluptuous as vol import re import io from typing import List, Dict import json
from PIL import Image, ImageDraw, UnidentifiedImageError from pathlib import Path
from homeassistant.components.image_processing import ( CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingEntity, ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util from homeassistant.util.pil import draw_box
_LOGGER = logging.getLogger(name)
PLATE_READER_URL = "https://api.platerecognizer.com/v1/plate-reader/" STATS_URL = "https://api.platerecognizer.com/v1/statistics/"
EVENT_VEHICLE_DETECTED = "platerecognizer.vehicle_detected"
ATTR_PLATE = "plate" ATTR_CONFIDENCE = "confidence" ATTR_REGION_CODE = "region_code" ATTR_VEHICLE_TYPE = "vehicle_type" ATTR_ORIENTATION = "orientation" ATTR_BOX_Y_CENTRE = "box_y_centre" ATTR_BOX_X_CENTRE = "box_x_centre" ATTR_ALLOWED_VEHICLE = "allowed_vehicle_detected"
CONF_API_TOKEN = "api_token" CONF_REGIONS = "regions" CONF_SAVE_FILE_FOLDER = "save_file_folder" CONF_SAVE_TIMESTAMPTED_FILE = "save_timestamped_file" CONF_ALWAYS_SAVE_LATEST_FILE = "always_save_latest_file" CONF_WATCHED_PLATES = "watched_plates" CONF_MMC = "mmc" CONF_SERVER = "server" CONF_DETECTION_RULE = "detection_rule" CONF_REGION_STRICT = "region"
DATETIMEFORMAT = "%Y-%m-%d%H-%M-%S" RED = (255, 0, 0) # For objects within the ROI DEFAULT_REGIONS = ['None']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_TOKEN): cv.string, vol.Optional(CONF_REGIONS, default=DEFAULT_REGIONS): vol.All( cv.ensure_list, [cv.string] ), vol.Optional(CONF_MMC, default=False): cv.boolean, vol.Optional(CONF_SAVE_FILE_FOLDER): cv.isdir, vol.Optional(CONF_SAVE_TIMESTAMPTED_FILE, default=False): cv.boolean, vol.Optional(CONF_ALWAYS_SAVE_LATEST_FILE, default=False): cv.boolean, vol.Optional(CONF_WATCHED_PLATES): vol.All( cv.ensure_list, [cv.string] ), vol.Optional(CONF_SERVER, default=PLATE_READER_URL): cv.string, vol.Optional(CONF_DETECTION_RULE, default=False): cv.string, vol.Optional(CONF_REGION_STRICT, default=False): cv.string, } )
def get_plates(results : List[Dict]) -> List[str]: """ Return the list of candidate plates. If no plates empty list returned. """ plates = [] candidates = [result['candidates'] for result in results] for candidate in candidates: cand_plates = [cand['plate'] for cand in candidate] for plate in cand_plates: plates.append(plate) return list(set(plates))
def get_orientations(results : List[Dict]) -> List[str]: """ Return the list of candidate orientations. If no orientations empty list returned. """ try: orientations = [] candidates = [result['orientation'] for result in results] for candidate in candidates: for cand in candidate: _LOGGER.debug("get_orientations cand: %s", cand) if cand["score"] >= 0.7: orientations.append(cand["orientation"]) return list(set(orientations)) except Exception as exc: _LOGGER.error("get_orientations error: %s", exc)
def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the platform."""
_LOGGER.info("*** Plate Recognition - Setup Platform")
save_file_folder = config.get(CONF_SAVE_FILE_FOLDER)
if save_file_folder:
save_file_folder = Path(save_file_folder)
entities = []
for camera in config[CONF_SOURCE]:
platerecognizer = PlateRecognizerEntity(
api_token=config.get(CONF_API_TOKEN),
regions = config.get(CONF_REGIONS),
save_file_folder=save_file_folder,
save_timestamped_file=config.get(CONF_SAVE_TIMESTAMPTED_FILE),
always_save_latest_file=config.get(CONF_ALWAYS_SAVE_LATEST_FILE),
watched_plates=config.get(CONF_WATCHED_PLATES),
camera_entity=camera[CONF_ENTITY_ID],
name=camera.get(CONF_NAME),
mmc=config.get(CONF_MMC),
server=config.get(CONF_SERVER),
detection_rule = config.get(CONF_DETECTION_RULE),
region_strict = config.get(CONF_REGION_STRICT),
allowed_vehicle_detected = False,
)
entities.append(platerecognizer)
add_entities(entities)
class PlateRecognizerEntity(ImageProcessingEntity): """Create entity."""
def __init__(
self,
api_token,
regions,
save_file_folder,
save_timestamped_file,
always_save_latest_file,
watched_plates,
camera_entity,
name,
mmc,
server,
detection_rule,
region_strict,
allowed_vehicle_detected,
):
"""Init."""
self._headers = {"Authorization": f"Token {api_token}"}
self._regions = regions
self._camera = camera_entity
if name:
self._name = name
else:
camera_name = split_entity_id(camera_entity)[1]
self._name = f"platerecognizer_{camera_name}"
self._save_file_folder = save_file_folder
self._save_timestamped_file = save_timestamped_file
self._always_save_latest_file = always_save_latest_file
self._watched_plates = watched_plates
self._mmc = mmc
self._server = server
self._detection_rule = detection_rule
self._region_strict = region_strict
self._state = None
self._results = {}
self._vehicles = [{}]
self._orientations = []
self._plates = []
self._statistics = {}
self._last_detection = None
self._image_width = None
self._image_height = None
self._image = None
self._config = {}
self.get_statistics()
self._last_file_timestamp = None
self.allowed_vehicle_detected = False
def process_image(self, image):
"""Process an image."""
self._state = None
self._results = {}
self._vehicles = [{}]
self._plates = []
self._orientations = []
_LOGGER.info("*** Plate Recognition - Image to scan: ")
self._image = Image.open(io.BytesIO(bytearray(image)))
self._image_width, self._image_height = self._image.size
if self._regions == DEFAULT_REGIONS:
regions = None
else:
regions = self._regions
if self._detection_rule:
self._config.update({"detection_rule" : self._detection_rule})
if self._region_strict:
self._config.update({"region": self._region_strict})
try:
_LOGGER.info("Config: " + str(json.dumps(self._config)))
response = requests.post(
self._server,
data=dict(regions=regions, camera_id=self.name, mmc=self._mmc, config=json.dumps(self._config)),
files={"upload": image},
headers=self._headers
).json()
_LOGGER.info("*** Plate Recognition - Just called API")
_LOGGER.info("***Plate Recognition Response*** %s", response)
self._results = response["results"]
self._plates = get_plates(response['results'])
if self._mmc:
self._orientations = get_orientations(response['results'])
self._vehicles = [
{
ATTR_PLATE: r["plate"],
ATTR_CONFIDENCE: r["score"],
ATTR_REGION_CODE: r["region"]["code"],
ATTR_VEHICLE_TYPE: r["vehicle"]["type"],
ATTR_BOX_Y_CENTRE: (r["box"]["ymin"] + ((r["box"]["ymax"] - r["box"]["ymin"]) /2)),
ATTR_BOX_X_CENTRE: (r["box"]["xmin"] + ((r["box"]["xmax"] - r["box"]["xmin"]) /2)),
}
for r in self._results
]
except Exception as exc:
_LOGGER.error("platerecognizer error: %s", exc)
_LOGGER.error(f"platerecognizer api response: {response}")
self._state = len(self._vehicles)
self._last_file_timestamp = dt_util.now().strftime(DATETIME_FORMAT)
self.allowed_vehicle_detected = False
_LOGGER.info("***+++ Plate Recognizer // RESETTING allowed_vehicle_detected = %s +++***", self.allowed_vehicle_detected)
if self._state > 0:
self._last_detection = dt_util.now().strftime(DATETIME_FORMAT)
for vehicle in self._vehicles:
self.fire_vehicle_detected_event(vehicle)
self.allowed_vehicle_detected = True
_LOGGER.info("***+++ Plate Recognizer // SETTING allowed_vehicle_detected = %s because %s +++***", self.allowed_vehicle_detected,vehicle)
if self._save_file_folder:
if self._state > 0 or self._always_save_latest_file:
self.save_image()
if self._server == PLATE_READER_URL:
self.get_statistics()
else:
stats = response["usage"]
calls_remaining = stats["max_calls"] - stats["calls"]
stats.update({"calls_remaining": calls_remaining})
self._statistics = stats
def get_statistics(self):
try:
response = requests.get(STATS_URL, headers=self._headers).json()
calls_remaining = response["total_calls"] - response["usage"]["calls"]
response.update({"calls_remaining": calls_remaining})
self._statistics = response.copy()
except Exception as exc:
_LOGGER.error("platerecognizer error getting statistics: %s", exc)
def fire_vehicle_detected_event(self, vehicle):
"""Send event."""
vehicle_copy = vehicle.copy()
vehicle_copy.update({ATTR_ENTITY_ID: self.entity_id})
self.hass.bus.fire(EVENT_VEHICLE_DETECTED, vehicle_copy)
def save_image(self):
"""Save a timestamped image with bounding boxes around plates."""
draw = ImageDraw.Draw(self._image)
decimal_places = 3
for vehicle in self._results:
box = (
round(vehicle['box']["ymin"] / self._image_height, decimal_places),
round(vehicle['box']["xmin"] / self._image_width, decimal_places),
round(vehicle['box']["ymax"] / self._image_height, decimal_places),
round(vehicle['box']["xmax"] / self._image_width, decimal_places),
)
text = vehicle['plate']
_LOGGER.info("***+++ VEICULO %s at +++***", text)
draw_box(
draw,
box,
self._image_width,
self._image_height,
text=text,
color=RED,
)
latest_save_path = self._save_file_folder / f"{self._name}_latest.png"
self._image.save(latest_save_path)
if self._save_timestamped_file:
timestamp_save_path = self._save_file_folder / f"{self._name}_{self._last_file_timestamp}.png"
self._image.save(timestamp_save_path)
_LOGGER.info("platerecognizer saved file %s", timestamp_save_path)
@property
def camera_entity(self):
"""Return camera entity id from process pictures."""
return self._camera
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def should_poll(self):
"""Return the polling state."""
return False
@property
def state(self):
"""Return the state of the entity."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return ATTR_PLATE
@property
def extra_state_attributes(self):
"""Return the attributes."""
attr = {}
attr.update({"last_detection": self._last_detection})
attr.update({"vehicles": self._vehicles})
attr.update({ATTR_ORIENTATION: self._orientations})
self.allowed_vehicle_detected = False
if self._watched_plates:
watched_plates_results = {plate : False for plate in self._watched_plates}
for plate in self._watched_plates:
if plate in self._plates:
watched_plates_results.update({plate: True})
self.allowed_vehicle_detected = True
attr[CONF_WATCHED_PLATES] = watched_plates_results
attr.update({"statistics": self._statistics})
attr.update({ATTR_ALLOWED_VEHICLE: self.allowed_vehicle_detected})
if self._regions != DEFAULT_REGIONS:
attr[CONF_REGIONS] = self._regions
if self._server != PLATE_READER_URL:
attr[CONF_SERVER] = str(self._server)
if self._save_file_folder:
attr[CONF_SAVE_FILE_FOLDER] = str(self._save_file_folder)
attr[CONF_SAVE_TIMESTAMPTED_FILE] = self._save_timestamped_file
attr[CONF_ALWAYS_SAVE_LATEST_FILE] = self._always_save_latest_file
return attr
You could also use the 'last_detection' attribute which is a timestamp. Then you can use that timestamp in the automation to check that if last_detection time is after the time the gate was last closed (ie: it is a new detection), then go ahead and open the gate.
Interesting, thanks for the tip. Awesome you actually answered the comment :-) btw, I also did some mods. (i was fighting my ignorance around py and hass). I changed the record of image to always save the image for debug purposes. When someone approaches the gate the image fires the motion event. Sometimes it takes the picture too soon or too late and the plate gets blurry / unfocused. Also for the non recognized plates for due diligence is also nice to see you tried to enter. for sure you could do a much better code than I can do.
then again I just could run an automation based on the triggered event right? maybe we can have different events, one for recognized plates but other for recognized wacthed_plates. Actually I think I have a bug because I turn on allowed plates in the block where you fire the event, but at that point any plate that is extracted, watched or not , fires the event.
Anyway, kudos for the great work. Always fun to get back coding a bit
*Gonçalo Caeiro - *Co-Founder and Chairman
Mobile: +1 (857)-316-5953 ; +351 917828727 | E-mail: @.***
www.infosistema.com| LinkedIn https://www.linkedin.com/company/infosistema | Facebook https://www.facebook.com/aInfosistema | Vimeo https://vimeo.com/infosistema
*Please consider the environment before printing this e-mail.*The information transmitted in this electronic mail message is intended for the sole use of the person or entity to which it is addressed and may be confidential or legally protected. Non-authorized use, copy, retransmission or dissemination of the information contained in this message is strictly forbidden.
On Thu, Feb 8, 2024 at 11:13 PM doenau @.***> wrote:
You could also use the 'last_detection' attribute which is a timestamp. Then you can use that timestamp in the automation to check that if last_detection time is after the time the gate was last closed (ie: it is a new detection), then go ahead and open the gate.
— Reply to this email directly, view it on GitHub https://github.com/robmarkcole/HASS-plate-recognizer/issues/82#issuecomment-1935078592, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAVW4RQHVNHF2GBTGLPVG43YSVL2DAVCNFSM6AAAAAA26C66UCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTSMZVGA3TQNJZGI . You are receiving this because you commented.Message ID: @.***>
Interesting, thanks for the tip. Awesome you actually answered the comment :-) btw, I also did some mods. (i was fighting my ignorance around py and hass).....
I'm just a user, not the integration owner :)
anyway.. re: your comment about the automations... I use my camera's cross-line detection as the trigger for the main plate-check automation... Then as individual separate automations, I detect whether the plate entity changes to True.
Plate-check automation:
alias: Vehicle Arrived - Plate Check
description: ""
trigger:
- platform: state
entity_id:
- binary_sensor.driveway_cross_line_alarm
- binary_sensor.front_yard_cross_line_alarm
to: "on"
from: "off"
condition: []
action:
- service: notify.mobile_app
data:
title: Vehicle Arrived
message: A car has pulled up the driveway
data:
image: /local/images/plates/platerecognizer_driveway_main_latest.png
url: /lovelace-test/driveway
enabled: false
- service: image_processing.scan
data: {}
target:
entity_id: image_processing.platerecognizer_driveway_main
- delay:
hours: 0
minutes: 30
seconds: 0
milliseconds: 0
mode: single
Custom sensor for each plate:
plate_085zyt:
friendly_name: "085zyt"
value_template: "{{ state_attr('image_processing.platerecognizer_driveway_main', 'watched_plates')['085zyt'] }}"`
Automation to fire when "085zyt" is detected:
description: ""
trigger:
- platform: state
entity_id:
- sensor.plate_085zyt
to: "True"
from: "False"
condition: []
action:
- service: notify.mobile_app
data:
title: Vehicle arrived
message: XXXX is home
- service: notify.google_assistant_sdk
data:
target:
- Living room Display
- Bed 4 Display
- Main Bedroom Wifi
message: XXXX is home
- service: notify.lg_webos_tv_nano86tpa
data:
message: XXXX is home
mode: single
ahahahha, even for kudos then. but aren't you managing the platerecognizer source code ?
*Gonçalo Caeiro - *Co-Founder and Chairman
Mobile: +1 (857)-316-5953 ; +351 917828727 | E-mail: @.***
www.infosistema.com| LinkedIn https://www.linkedin.com/company/infosistema | Facebook https://www.facebook.com/aInfosistema | Vimeo https://vimeo.com/infosistema
*Please consider the environment before printing this e-mail.*The information transmitted in this electronic mail message is intended for the sole use of the person or entity to which it is addressed and may be confidential or legally protected. Non-authorized use, copy, retransmission or dissemination of the information contained in this message is strictly forbidden.
On Fri, Feb 9, 2024 at 12:55 AM doenau @.***> wrote:
Interesting, thanks for the tip. Awesome you actually answered the comment :-) btw, I also did some mods. (i was fighting my ignorance around py and hass).....
I'm just a user, not the integration owner :)
anyway.. re: your comment about the automations... I use my camera's cross-line detection as the trigger for the main plate-check automation... Then as individual separate automations, I detect whether the plate entity changes to True.
Plate-check automation:
alias: Vehicle Arrived - Plate Check description: "" trigger:
- platform: state entity_id:
- binary_sensor.driveway_cross_line_alarm
- binary_sensor.front_yard_cross_line_alarm to: "on" from: "off" condition: [] action:
- service: notify.mobile_app_andy data: title: Vehicle Arrived message: A car has pulled up the driveway data: image: /local/images/plates/platerecognizer_driveway_main_latest.png url: /lovelace-test/driveway enabled: false
- service: image_processing.scan data: {} target: entity_id: image_processing.platerecognizer_driveway_main
- delay: hours: 0 minutes: 30 seconds: 0 milliseconds: 0 mode: single
Custom sensor for each plate:
plate_085zyt: friendly_name: "085zyt" value_template: "{{ state_attr('image_processing.platerecognizer_driveway_main', 'watched_plates')['085zyt'] }}"`
Automation to fire when "085zyt" is detected: `alias: Vehicle arrived - 085zyt description: "" trigger:
- platform: state entity_id:
- sensor.plate_085zyt to: "True" from: "False" condition: [] action:
- service: notify.mobile_app data: title: Vehicle arrived message: XXXX is home
- service: notify.google_assistant_sdk data: target:
- Living room Display
- Bed 4 Display
- Main Bedroom Wifi message: XXXX is home
- service: notify.lg_webos_tv_nano86tpa data: message: XXXX is home mode: single
— Reply to this email directly, view it on GitHub https://github.com/robmarkcole/HASS-plate-recognizer/issues/82#issuecomment-1935164617, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAVW4RRATBU32SM62LK3EJDYSVXY5AVCNFSM6AAAAAA26C66UCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTSMZVGE3DINRRG4 . You are receiving this because you commented.Message ID: @.***>
Honestly, this is something the whole "HA" world is missing...partially I can understand why (the majority of entities are exposed by integrations that should take care of it) but mostly, imho, it seems a de-facto choice in the HA world.
Anyhow...the only way to mitigate this issue is to use an old script, which still works fortunately: https://github.com/xannor/hass_py_set_state
(there is also a more recent version here but I'm unsure about it)
Please note, despite what stated in the readme you can not install it trough HACS: you need to manually create the
phyton_scripts
folder in "config" folder of your HA, and then you need to add thepython_script:
in yourconfiguration.yaml
. Reload your HA instance and follow the examples on the readme for how to call/set the service.
I used the script to reset watched plates to 0 as well as the detected plates to "false". Except that when I trigger the plate reading, it displays the last plate read for 2 seconds before displaying the new one... do you have a yaml to share to test?
At the moment, the detected vehicle count remains the same until the next API call, as do the 'watched plates' true/false states. It would be helpful to have these (optional configuration) reset to 0 and 'false' again after a few minutes (perhaps optional configuration as part of the yaml config) so they are a better representation of the current state.