anpr2mqtt 0.10.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,195 @@
1
+ Metadata-Version: 2.4
2
+ Name: anpr2mqtt
3
+ Version: 0.10.1
4
+ Summary: ANPR MQTT bridge for Home Assistant
5
+ Author: jey burrows
6
+ Author-email: jey burrows <jrb@rhizomatics.org.uk>
7
+ License-Expression: Apache-2.0
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Natural Language :: English
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Environment :: Console
12
+ Classifier: Topic :: Home Automation
13
+ Classifier: Topic :: System :: Monitoring
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: Typing :: Typed
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Requires-Dist: paho-mqtt>=2.1.0
19
+ Requires-Dist: pillow>=12.0.0
20
+ Requires-Dist: pytesseract>=0.3.13
21
+ Requires-Dist: structlog>=25.4.0
22
+ Requires-Dist: rich>=14.0.0
23
+ Requires-Dist: watchdog>=6.0.0
24
+ Requires-Dist: tzlocal>=5.3.1
25
+ Requires-Dist: usingversion>=0.1.2
26
+ Requires-Dist: niquests>=3.0.0
27
+ Requires-Dist: requests-cache>=1.0.0
28
+ Requires-Dist: pydantic-settings>=2.12.0
29
+ Requires-Dist: rapidfuzz>=3.14.5
30
+ Requires-Python: >=3.13
31
+ Description-Content-Type: text/markdown
32
+
33
+ ![anpr2mqtt](assets/images/anpr2mqttdark-256x256.png){ align=left }
34
+
35
+ # anpr2mqtt
36
+
37
+ [![Rhizomatics Open Source](https://img.shields.io/badge/rhizomatics%20open%20source-lightseagreen)](https://github.com/rhizomatics)
38
+
39
+
40
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/rhizomatics/anpr2mqtt)
41
+ [![Coverage](https://raw.githubusercontent.com/rhizomatics/anpr2mqtt/refs/heads/badges/badges/coverage.svg)](https://anpr2mqtt.rhizomatics.org.uk/developer/coverage/)
42
+ ![Tests](https://raw.githubusercontent.com/rhizomatics/anpr2mqtt/refs/heads/badges/badges/tests.svg)
43
+ [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/rhizomatics/anpr2mqtt/main.svg)](https://results.pre-commit.ci/latest/github/rhizomatics/anpr2mqtt/main)
44
+ [![Github Deploy](https://github.com/rhizomatics/anpr2mqtt/actions/workflows/deploy.yml/badge.svg?branch=main)](https://github.com/rhizomatics/anpr2mqtt/actions/workflows/deploy.yml)
45
+ [![CodeQL](https://github.com/rhizomatics/anpr2mqtt/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/rhizomatics/anpr2mqtt/actions/workflows/github-code-scanning/codeql)
46
+ [![Dependabot Updates](https://github.com/rhizomatics/anpr2mqtt/actions/workflows/dependabot/dependabot-updates/badge.svg)](https://github.com/rhizomatics/anpr2mqtt/actions/workflows/dependabot/dependabot-updates)
47
+
48
+
49
+ # ANPR MQTT Bridge
50
+
51
+ A simple way to integrate CCTV cameras with built-in **ANPR** (**Automatic Number Plate Recognition**, aka **ALPR** or **Automatic License Plate Recognition**) to MQTT for Home Assistant integration, or any other MQTT consumer. Running under Docker is preferred but not necessary.
52
+
53
+ All that is needed is for the camera to be configured to upload images on plate recognition, by ftp, NAS or whatever else. ANPR2MQTT monitors the directory where the images lands and publishes plate information to MQTT. Its simple, requires no proprietary vendor APIs ( or differing ONVIF implementations ), and Home Assistant gets a copy of the actual annotated detection image to use on dashboards or to attach to notifications.
54
+
55
+ While intended for vehicle plate detection, it can be used to watch for and analyze any file, so for example uploaded face detection or line crossing images. A single `anpr2mqtt` instance can watch multiple paths and patterns for different cameras and events.
56
+
57
+ ## Features
58
+
59
+ * Minimal configuration
60
+ - Ready configured to work with popular Hikvision ANPR camera settings
61
+ - See [Example Camera Configuration](camera.md)
62
+ * File System Integration
63
+ - Watches directory for ANPR camera images using [Watchdog](https://python-watchdog.readthedocs.io/en/stable/index.html)
64
+ - Uses [inotify](https://www.man7.org/linux/man-pages/man7/inotify.7.html) on Linux, or equiv on other operating systems for efficient listening to file system events without continual polling
65
+ - Extracts target ( for example licence plate ), timestamp and event type information from filenames
66
+ * [Home Assistant Integration](homeassistant.md).
67
+ - Publishes events to MQTT for Home Assistant as a [MQTT Sensor Entity](https://www.home-assistant.io/integrations/sensor.mqtt/)
68
+ - Auto-discovery configuration for Home Assistant
69
+ - Creates [MQTT Image Entity](https://www.home-assistant.io/integrations/image.mqtt/) on Home Assistant for image snapshot, so no web access to ftp needed
70
+ - Optionally also creates an [MQTT Camera Entity](https://www.home-assistant.io/integrations/camera.mqtt/)
71
+ * Plate Enrichment
72
+ - OCR-based extraction of fields using [tesseract-ocr](https://github.com/tesseract-ocr/tesseract)
73
+ - By default direction detection (Forward/Reverse)
74
+ - Corrections by regular expression to fix OCR mis-readings
75
+ - Tracks and counts previous sightings
76
+ - Configurable to classify plates as known, to be ignored or as a potential threat
77
+ - Regular expression based corrections, for known plates that the ANPR sometimes mis-reads
78
+ - Fuzzy match corrections for known plates, based on Levenshtein algorithm
79
+ - UK Only
80
+ - [DVLA Lookup](api.md) if API_KEY provided, for detailed MOT and tax information
81
+ - Lookups cached for configurable time
82
+ * Auto clear vehicle state optionally, after configurable time
83
+ * [Debug Tools](debug_tools.md) built-in
84
+
85
+
86
+ ## Docker Deployment
87
+
88
+ Build and run with Docker, example [docker-compose.yaml](examples/docker_compose.md) provided.
89
+
90
+ ## Configuration
91
+
92
+ ANPR2MQTT uses [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/), which means configuration can happen in a variety of ways, and these can be combined - yaml configuration file, environment variables, Docker Secrets, built-in defaults, and `.env` file or command line arguments.
93
+
94
+ ### CLI Arguments
95
+
96
+ Every configuration setting can be passed as a command line argument, using dot notation for nested settings.
97
+ This is the highest-priority source and overrides all other configuration.
98
+
99
+ ```bash
100
+ uv run anpr2mqtt --mqtt.host 192.168.1.10 --mqtt.port 1884 --log_level DEBUG
101
+ ```
102
+
103
+ Run `uv run anpr2mqtt --help` to see all available flags, and find more information at [Debug Tools](debug_tools.md)
104
+
105
+ ## Home Assistant Integration
106
+
107
+ See [Home Assistant Integration](homeassistant.md) for configuration and example notification automation.
108
+
109
+ ### Environment Variables
110
+
111
+ [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) needs double underscores for
112
+ environment variables in config sections. For example, `MQTT__HOST` means set the value `host` in the `mqtt` config section.
113
+
114
+ | Env Variable | Description | Default |
115
+ |-------------------------------|-------------|---------|
116
+ | `MQTT__HOST` | MQTT broker hostname | `localhost` |
117
+ | `MQTT__PORT` | MQTT broker port | `1883` |
118
+ | `MQTT__TOPIC_ROOT` | MQTT topic for events | `anpr2mqtt` |
119
+ | `MQTT__USER` | MQTT username | - |
120
+ | `MQTT__PASS` | MQTT password | - |
121
+ | `LOG_LEVEL` | Python logging level | INFO |
122
+ | `DVLA__API_KEY` | API Key for Gov API Lookup | - |
123
+
124
+
125
+ ## Image Filename Format
126
+
127
+ Expected format: `YYYYMMDDHHMMSSmmm_PLATE_VEHICLE_DETECTION.jpg`
128
+
129
+ Example: `20180502174029596_A2GEO_VEHICLE_DETECTION.jpg`
130
+
131
+ A regular expression can be defined to match different file name formats.
132
+
133
+ ## Image Box Coordinates
134
+
135
+ Where cameras provide an estimated direction for the vehicle, this can be captured via OCR and
136
+ included in the response. See [OCR](ocr.md) for an explanation and examples.
137
+
138
+ ## Auto Clear
139
+
140
+ By default, the state will be reset back to unavailable 5 minutes after a detection, while the image will
141
+ be left present as record of last known vehicle. This behavior can be changed ( to switch off auto clear,
142
+ or change the time lag, or also clear image ) using the `autoclear` configuration for each event.
143
+
144
+ ```yaml title="configuration snippet"
145
+ - camera: shed
146
+ watch_path: /ftp/shedcam
147
+ autoclear:
148
+ post_event: 360
149
+ state: True
150
+ image: False
151
+ ```
152
+
153
+ ## Corrections
154
+
155
+ The licence plate detection may mis-read or miss some of the characters of the plate. When the result is
156
+ published to MQTT, both the raw original and corrected versions are provided.
157
+
158
+ Two mechanisms help with this:
159
+
160
+ ### Fuzzy Matching
161
+
162
+ The Levenshtein method is used to compare the plate against a list of known friendly or dangerous
163
+ plates in the configuration, subject to a maximum distance tolerance ( number of mismatched characters )
164
+ defined by `auto_match_tolerance`. If its a match for more than one, the plate with least distance is chosen.
165
+
166
+ ### Regex / String Matching
167
+
168
+ Each known plate can be associated with a list of regular expressions and/or plain strings, and these will
169
+ be checked for every discovered licence plate.
170
+
171
+ ## Primary Dependencies
172
+
173
+ - **watchdog** - File system monitoring (cross-platform)
174
+ - **paho-mqtt** - MQTT client
175
+ - **Pillow** - Image processing
176
+ - **pytesseract** - OCR for direction detection
177
+ - **structlog** - Structured logging
178
+ - **niquests** - API Client
179
+ - **requests-cache** - API result caching
180
+
181
+ ## Distribution
182
+
183
+ ANPR2MQTT is free and open sourced under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0).
184
+
185
+ ## Rhizomatics Open Source for Home Assistant
186
+
187
+ ### HACS
188
+ - [AutoArm](https://autoarm.rhizomatics.org.uk) - Automatically arm and disarm Home Assistant alarm control panels using physical buttons, presence, calendars, sun and more
189
+ - [Remote Logger](https://remote-logger.rhizomatics.org.uk) - OpenTelemetry (OTLP) and Syslog event capture for Home Assistant
190
+ - [Supernotify](https://supernotify.rhizomatics.org.uk) - Unified notification for easy multi-channel messaging, including powerful chime and security camera integration.
191
+
192
+
193
+ ### Python / Docker
194
+
195
+ - [Updates2MQTT](https://updates2mqtt.rhizomatics.org.uk) - Automatically notify via MQTT on Docker image updates, with advanced handling to extract versions and release notes from images, and option to remotely pull and restart containers from Home Assistant. Also available on [PyPI](https://pypi.org/project/updates2mqtt/)
@@ -0,0 +1,163 @@
1
+ ![anpr2mqtt](assets/images/anpr2mqttdark-256x256.png){ align=left }
2
+
3
+ # anpr2mqtt
4
+
5
+ [![Rhizomatics Open Source](https://img.shields.io/badge/rhizomatics%20open%20source-lightseagreen)](https://github.com/rhizomatics)
6
+
7
+
8
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/rhizomatics/anpr2mqtt)
9
+ [![Coverage](https://raw.githubusercontent.com/rhizomatics/anpr2mqtt/refs/heads/badges/badges/coverage.svg)](https://anpr2mqtt.rhizomatics.org.uk/developer/coverage/)
10
+ ![Tests](https://raw.githubusercontent.com/rhizomatics/anpr2mqtt/refs/heads/badges/badges/tests.svg)
11
+ [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/rhizomatics/anpr2mqtt/main.svg)](https://results.pre-commit.ci/latest/github/rhizomatics/anpr2mqtt/main)
12
+ [![Github Deploy](https://github.com/rhizomatics/anpr2mqtt/actions/workflows/deploy.yml/badge.svg?branch=main)](https://github.com/rhizomatics/anpr2mqtt/actions/workflows/deploy.yml)
13
+ [![CodeQL](https://github.com/rhizomatics/anpr2mqtt/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/rhizomatics/anpr2mqtt/actions/workflows/github-code-scanning/codeql)
14
+ [![Dependabot Updates](https://github.com/rhizomatics/anpr2mqtt/actions/workflows/dependabot/dependabot-updates/badge.svg)](https://github.com/rhizomatics/anpr2mqtt/actions/workflows/dependabot/dependabot-updates)
15
+
16
+
17
+ # ANPR MQTT Bridge
18
+
19
+ A simple way to integrate CCTV cameras with built-in **ANPR** (**Automatic Number Plate Recognition**, aka **ALPR** or **Automatic License Plate Recognition**) to MQTT for Home Assistant integration, or any other MQTT consumer. Running under Docker is preferred but not necessary.
20
+
21
+ All that is needed is for the camera to be configured to upload images on plate recognition, by ftp, NAS or whatever else. ANPR2MQTT monitors the directory where the images lands and publishes plate information to MQTT. Its simple, requires no proprietary vendor APIs ( or differing ONVIF implementations ), and Home Assistant gets a copy of the actual annotated detection image to use on dashboards or to attach to notifications.
22
+
23
+ While intended for vehicle plate detection, it can be used to watch for and analyze any file, so for example uploaded face detection or line crossing images. A single `anpr2mqtt` instance can watch multiple paths and patterns for different cameras and events.
24
+
25
+ ## Features
26
+
27
+ * Minimal configuration
28
+ - Ready configured to work with popular Hikvision ANPR camera settings
29
+ - See [Example Camera Configuration](camera.md)
30
+ * File System Integration
31
+ - Watches directory for ANPR camera images using [Watchdog](https://python-watchdog.readthedocs.io/en/stable/index.html)
32
+ - Uses [inotify](https://www.man7.org/linux/man-pages/man7/inotify.7.html) on Linux, or equiv on other operating systems for efficient listening to file system events without continual polling
33
+ - Extracts target ( for example licence plate ), timestamp and event type information from filenames
34
+ * [Home Assistant Integration](homeassistant.md).
35
+ - Publishes events to MQTT for Home Assistant as a [MQTT Sensor Entity](https://www.home-assistant.io/integrations/sensor.mqtt/)
36
+ - Auto-discovery configuration for Home Assistant
37
+ - Creates [MQTT Image Entity](https://www.home-assistant.io/integrations/image.mqtt/) on Home Assistant for image snapshot, so no web access to ftp needed
38
+ - Optionally also creates an [MQTT Camera Entity](https://www.home-assistant.io/integrations/camera.mqtt/)
39
+ * Plate Enrichment
40
+ - OCR-based extraction of fields using [tesseract-ocr](https://github.com/tesseract-ocr/tesseract)
41
+ - By default direction detection (Forward/Reverse)
42
+ - Corrections by regular expression to fix OCR mis-readings
43
+ - Tracks and counts previous sightings
44
+ - Configurable to classify plates as known, to be ignored or as a potential threat
45
+ - Regular expression based corrections, for known plates that the ANPR sometimes mis-reads
46
+ - Fuzzy match corrections for known plates, based on Levenshtein algorithm
47
+ - UK Only
48
+ - [DVLA Lookup](api.md) if API_KEY provided, for detailed MOT and tax information
49
+ - Lookups cached for configurable time
50
+ * Auto clear vehicle state optionally, after configurable time
51
+ * [Debug Tools](debug_tools.md) built-in
52
+
53
+
54
+ ## Docker Deployment
55
+
56
+ Build and run with Docker, example [docker-compose.yaml](examples/docker_compose.md) provided.
57
+
58
+ ## Configuration
59
+
60
+ ANPR2MQTT uses [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/), which means configuration can happen in a variety of ways, and these can be combined - yaml configuration file, environment variables, Docker Secrets, built-in defaults, and `.env` file or command line arguments.
61
+
62
+ ### CLI Arguments
63
+
64
+ Every configuration setting can be passed as a command line argument, using dot notation for nested settings.
65
+ This is the highest-priority source and overrides all other configuration.
66
+
67
+ ```bash
68
+ uv run anpr2mqtt --mqtt.host 192.168.1.10 --mqtt.port 1884 --log_level DEBUG
69
+ ```
70
+
71
+ Run `uv run anpr2mqtt --help` to see all available flags, and find more information at [Debug Tools](debug_tools.md)
72
+
73
+ ## Home Assistant Integration
74
+
75
+ See [Home Assistant Integration](homeassistant.md) for configuration and example notification automation.
76
+
77
+ ### Environment Variables
78
+
79
+ [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) needs double underscores for
80
+ environment variables in config sections. For example, `MQTT__HOST` means set the value `host` in the `mqtt` config section.
81
+
82
+ | Env Variable | Description | Default |
83
+ |-------------------------------|-------------|---------|
84
+ | `MQTT__HOST` | MQTT broker hostname | `localhost` |
85
+ | `MQTT__PORT` | MQTT broker port | `1883` |
86
+ | `MQTT__TOPIC_ROOT` | MQTT topic for events | `anpr2mqtt` |
87
+ | `MQTT__USER` | MQTT username | - |
88
+ | `MQTT__PASS` | MQTT password | - |
89
+ | `LOG_LEVEL` | Python logging level | INFO |
90
+ | `DVLA__API_KEY` | API Key for Gov API Lookup | - |
91
+
92
+
93
+ ## Image Filename Format
94
+
95
+ Expected format: `YYYYMMDDHHMMSSmmm_PLATE_VEHICLE_DETECTION.jpg`
96
+
97
+ Example: `20180502174029596_A2GEO_VEHICLE_DETECTION.jpg`
98
+
99
+ A regular expression can be defined to match different file name formats.
100
+
101
+ ## Image Box Coordinates
102
+
103
+ Where cameras provide an estimated direction for the vehicle, this can be captured via OCR and
104
+ included in the response. See [OCR](ocr.md) for an explanation and examples.
105
+
106
+ ## Auto Clear
107
+
108
+ By default, the state will be reset back to unavailable 5 minutes after a detection, while the image will
109
+ be left present as record of last known vehicle. This behavior can be changed ( to switch off auto clear,
110
+ or change the time lag, or also clear image ) using the `autoclear` configuration for each event.
111
+
112
+ ```yaml title="configuration snippet"
113
+ - camera: shed
114
+ watch_path: /ftp/shedcam
115
+ autoclear:
116
+ post_event: 360
117
+ state: True
118
+ image: False
119
+ ```
120
+
121
+ ## Corrections
122
+
123
+ The licence plate detection may mis-read or miss some of the characters of the plate. When the result is
124
+ published to MQTT, both the raw original and corrected versions are provided.
125
+
126
+ Two mechanisms help with this:
127
+
128
+ ### Fuzzy Matching
129
+
130
+ The Levenshtein method is used to compare the plate against a list of known friendly or dangerous
131
+ plates in the configuration, subject to a maximum distance tolerance ( number of mismatched characters )
132
+ defined by `auto_match_tolerance`. If its a match for more than one, the plate with least distance is chosen.
133
+
134
+ ### Regex / String Matching
135
+
136
+ Each known plate can be associated with a list of regular expressions and/or plain strings, and these will
137
+ be checked for every discovered licence plate.
138
+
139
+ ## Primary Dependencies
140
+
141
+ - **watchdog** - File system monitoring (cross-platform)
142
+ - **paho-mqtt** - MQTT client
143
+ - **Pillow** - Image processing
144
+ - **pytesseract** - OCR for direction detection
145
+ - **structlog** - Structured logging
146
+ - **niquests** - API Client
147
+ - **requests-cache** - API result caching
148
+
149
+ ## Distribution
150
+
151
+ ANPR2MQTT is free and open sourced under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0).
152
+
153
+ ## Rhizomatics Open Source for Home Assistant
154
+
155
+ ### HACS
156
+ - [AutoArm](https://autoarm.rhizomatics.org.uk) - Automatically arm and disarm Home Assistant alarm control panels using physical buttons, presence, calendars, sun and more
157
+ - [Remote Logger](https://remote-logger.rhizomatics.org.uk) - OpenTelemetry (OTLP) and Syslog event capture for Home Assistant
158
+ - [Supernotify](https://supernotify.rhizomatics.org.uk) - Unified notification for easy multi-channel messaging, including powerful chime and security camera integration.
159
+
160
+
161
+ ### Python / Docker
162
+
163
+ - [Updates2MQTT](https://updates2mqtt.rhizomatics.org.uk) - Automatically notify via MQTT on Docker image updates, with advanced handling to extract versions and release notes from images, and option to remotely pull and restart containers from Home Assistant. Also available on [PyPI](https://pypi.org/project/updates2mqtt/)
@@ -0,0 +1,187 @@
1
+ [project]
2
+ name = "anpr2mqtt"
3
+ version = "0.10.1"
4
+ description = "ANPR MQTT bridge for Home Assistant"
5
+ authors = [
6
+ { name = "jey burrows", email = "jrb@rhizomatics.org.uk" }
7
+ ]
8
+
9
+ readme = "README.md"
10
+ requires-python = ">=3.13"
11
+ license="Apache-2.0"
12
+ dependencies = [
13
+ "paho-mqtt>=2.1.0",
14
+ "pillow>=12.0.0",
15
+ "pytesseract>=0.3.13",
16
+ "structlog>=25.4.0",
17
+ "rich>=14.0.0",
18
+ "watchdog>=6.0.0",
19
+ "tzlocal>=5.3.1",
20
+ "usingversion>=0.1.2",
21
+ "niquests>=3.0.0",
22
+ "requests-cache>=1.0.0",
23
+ "pydantic-settings>=2.12.0",
24
+ "rapidfuzz>=3.14.5",
25
+ ]
26
+
27
+ classifiers = [
28
+ "Development Status :: 5 - Production/Stable",
29
+ "Natural Language :: English",
30
+ "Operating System :: OS Independent",
31
+ "Environment :: Console",
32
+ "Topic :: Home Automation",
33
+ "Topic :: System :: Monitoring",
34
+ "Intended Audience :: System Administrators",
35
+ "Typing :: Typed",
36
+ "Programming Language :: Python",
37
+ "Programming Language :: Python :: 3.14"
38
+ ]
39
+
40
+
41
+ [project.scripts]
42
+ anpr2mqtt = "anpr2mqtt:run"
43
+ tools = "anpr2mqtt:tools"
44
+
45
+ [tool.uv]
46
+ compile-bytecode = true
47
+ managed = true
48
+
49
+ [tool.uv.build-backend]
50
+ namespace = true
51
+ module-name = "anpr2mqtt"
52
+
53
+ [dependency-groups]
54
+ dev = [
55
+ "ruff>=0.13.0",
56
+ "pre-commit>=4.4.0",
57
+ "pytest>=8.0.0",
58
+ "pytest-mock>=3.15.0",
59
+ "pytest-mqtt>=0.6.0",
60
+ "pre-commit>=3.0.0",
61
+ "ruff>=0.5.0",
62
+ "mypy>=1.11.0",
63
+ "pytest-cov",
64
+ "coverage",
65
+ "icdiff",
66
+ "genbadge[all]",
67
+ "pytest-xdist",
68
+ "pytest-shell-utilities"
69
+ ]
70
+ docs=[
71
+ "mkdocs<2.0.0",
72
+ "mkdocs-material[imaging]",
73
+ "mkdocs-material>=9.7.1",
74
+ "mkdocstrings",
75
+ "mkdocstrings-python",
76
+ "mkdocs-minify-plugin",
77
+ "mkdocs-material-extensions",
78
+ "pymdown-extensions",
79
+ "mkdocs-autorefs",
80
+ "mkdocs-coverage",
81
+ "mkdocs-pagetree-plugin",
82
+ "mkdocs",
83
+ "mkdocs-git-revision-date-localized-plugin",
84
+ "mkdocs-minify-html-plugin",
85
+ "mkdocs-meta-descriptions-plugin",
86
+ "mike",
87
+ "mkdocs-htmlproofer-plugin"
88
+ ]
89
+ [build-system]
90
+ requires = ["uv_build>=0.11.3,<0.12.0"]
91
+ build-backend = "uv_build"
92
+
93
+ [mypy]
94
+ plugins = "pydantic.mypy"
95
+
96
+ [tool.bandit]
97
+ exclude_dirs = ["tests"]
98
+ skips = ["B311"]
99
+
100
+ [tool.ruff]
101
+ line-length = 128
102
+ indent-width = 4
103
+ target-version = "py313"
104
+ exclude = [
105
+ ".bzr",
106
+ ".direnv",
107
+ ".eggs",
108
+ ".git",
109
+ ".git-rewrite",
110
+ ".hg",
111
+ ".ipynb_checkpoints",
112
+ ".mypy_cache",
113
+ ".nox",
114
+ ".pants.d",
115
+ ".pyenv",
116
+ ".pytest_cache",
117
+ ".pytype",
118
+ ".ruff_cache",
119
+ ".svn",
120
+ ".tox",
121
+ ".venv",
122
+ ".vscode",
123
+ "__pypackages__",
124
+ "_build",
125
+ "buck-out",
126
+ "build",
127
+ "dist",
128
+ "htmlcov",
129
+ ".mypy_cache",
130
+ ".pytest_cache",
131
+ ".github",
132
+ "node_modules",
133
+ "site-packages",
134
+ "venv",
135
+ ]
136
+
137
+ [tool.ruff.lint]
138
+ select = ["E", "C","R", "F", "B", "W", "I", "C90", "N", "D", "UP", "YTT",
139
+ "RUF","PGH","PT","T20","DTZ","TD","PTH","ARG","INT","TCH","TID","SIM","PERF","ASYNC","ANN","S","A","COM","C4"]
140
+ ignore = [
141
+ "W191","E111","E114","E117","D206","D300","Q000","Q001","COM812","COM819","ISC001","ISC002", # std formatter conflicts
142
+ "D203","D213", # other formatter clashes
143
+ "TD002","TD003","TD005",
144
+ "D103","D100","D101","D102","D104","D107","D415","D400","D415","ANN401",
145
+ "S101","C901","FURB113",
146
+ "S602","S607","S404" ] # subprocess usage
147
+ unfixable = ["B"]
148
+
149
+
150
+ [tool.ruff.format]
151
+ # Like Black, use double quotes for strings.
152
+ quote-style = "double"
153
+
154
+ # Like Black, indent with spaces, rather than tabs.
155
+ indent-style = "space"
156
+
157
+ # Like Black, respect magic trailing commas.
158
+ skip-magic-trailing-comma = false
159
+
160
+ # Like Black, automatically detect the appropriate line ending.
161
+ line-ending = "auto"
162
+
163
+ [tool.pytest]
164
+ markers = [
165
+ "manual: tests to be executed manually"
166
+ ]
167
+ testpaths = ["tests"]
168
+ norecursedirs = [".git","templates"]
169
+ pythonpath = ["."]
170
+ addopts = ["--strict-markers","--junitxml=junit/test-results.xml",
171
+ "--cov","--cov-report=lcov:lcov.info","--cov-report=xml","--cov-report=html","--cov-report=term","--import-mode=importlib"]
172
+
173
+ [tool.coverage.run]
174
+ branch = true
175
+ source = ["src"]
176
+
177
+ [tool.coverage.report]
178
+ fail_under=0
179
+
180
+ [tool.coverage.xml]
181
+ output="cov.xml"
182
+
183
+ [tool.coverage.html]
184
+ directory="htmlcov"
185
+
186
+ [tool.mypy]
187
+ mypy_path=["./src","./tests"]
@@ -0,0 +1,6 @@
1
+ from usingversion import getattr_with_version # type:ignore[import-not-found]
2
+
3
+ from anpr2mqtt.app import run as run
4
+ from anpr2mqtt.tools import tools as tools
5
+
6
+ __getattr__ = getattr_with_version("anpr2mqtt", __file__, __name__)
@@ -0,0 +1,6 @@
1
+ """Detect component version change availability and broadcast on MQTT topic, suitable for HomeAssistant autodetect"""
2
+
3
+ if __name__ == "__main__":
4
+ from .app import run
5
+
6
+ run()
@@ -0,0 +1,112 @@
1
+ import re
2
+ from pathlib import Path
3
+ from typing import TYPE_CHECKING, Any, Literal, cast
4
+
5
+ import niquests
6
+ import structlog
7
+ from requests_cache.backends.filesystem import FileCache
8
+ from requests_cache.session import CacheMixin
9
+
10
+ from anpr2mqtt.settings import CacheType
11
+
12
+ if TYPE_CHECKING:
13
+ from requests_cache.models.response import CachedResponse
14
+
15
+
16
+ class _CachedSession(CacheMixin, niquests.Session): # type: ignore[misc]
17
+ """requests-cache backed by niquests as the transport."""
18
+
19
+
20
+ log = structlog.get_logger()
21
+
22
+
23
+ class APIClient:
24
+ def lookup(self, reg: str) -> dict[str, Any]:
25
+ raise NotImplementedError()
26
+
27
+
28
+ class DVLAClient(APIClient):
29
+ ID = "GB"
30
+ REG_RE = r"(^[A-Z]{2}[0-9]{2}\s?[A-Z]{3}$)|(^[A-Z][0-9]{1,3}[A-Z]{3}$)|(^[A-Z]{3}[0-9]{1,3}[A-Z]$)|(^[0-9]{1,4}[A-Z]{1,2}$)|(^[0-9]{1,3}[A-Z]{1,3}$)|(^[A-Z]{1,2}[0-9]{1,4}$)|(^[A-Z]{1,3}[0-9]{1,3}$)|(^[A-Z]{1,3}[0-9]{1,4}$)|(^[0-9]{3}[DX]{1}[0-9]{3}$)" # noqa: E501
31
+ """https://developer-portal.driver-vehicle-licensing.api.gov.uk"""
32
+
33
+ def __init__(
34
+ self,
35
+ api_key: str,
36
+ cache_ttl: int = 60 * 60 * 6,
37
+ cache_type: CacheType = CacheType.MEMORY,
38
+ cache_dir: Path | None = None,
39
+ verify_plate: str | None = None,
40
+ test: bool = False,
41
+ ) -> None:
42
+ self.cache_session: _CachedSession | None = None
43
+ if cache_type == CacheType.FILE and cache_dir:
44
+ try:
45
+ file_cache: FileCache = FileCache(cache_name=str(cache_dir), use_cache_dir=True)
46
+ log.debug("Caching DVLA at %s for %s", file_cache.cache_dir, cache_ttl)
47
+ self.cache_session = _CachedSession(
48
+ cache_name="dvla_cache", allowable_methods=["GET", "POST"], expire_after=cache_ttl, backend=file_cache
49
+ )
50
+ except Exception as e:
51
+ log.error("Unable to configure file system caching, reverting to in memory: %s", e)
52
+ cache_type = CacheType.MEMORY
53
+
54
+ if self.cache_session is None:
55
+ if cache_type != CacheType.MEMORY:
56
+ log.warn("Unable to cache DVLA as %s, falling back to in memory for %s", cache_type, cache_ttl)
57
+ else:
58
+ log.debug("Caching DVLA in memory for %s", cache_ttl)
59
+ self.cache_session = _CachedSession(
60
+ cache_name="dvla_cache", allowable_methods=["GET", "POST"], backend="memory", expire_after=cache_ttl
61
+ )
62
+
63
+ self.api_key: str = api_key
64
+ self.env_prefix: Literal["uat."] | Literal[""] = "uat." if test else ""
65
+ if verify_plate:
66
+ result: dict[str, Any] = self.lookup(reg=verify_plate)
67
+ if result and result["success"]:
68
+ log.info("Verified DVLA API lookup using %s, details: %s", verify_plate, result["plate"])
69
+ else:
70
+ log.error("DVLA startup verificatio failed: %s", result)
71
+
72
+ def lookup(self, reg: str) -> dict[str, Any]:
73
+ if not re.match(self.REG_RE, reg):
74
+ log.warning(f"DVLA SKIP invalid reg {reg}")
75
+ return {"reg_match_fail": self.ID, "plate": {}, "success": False}
76
+ if self.cache_session is None:
77
+ log.error("Unable to lookup, failed to configure cache session or fallback")
78
+ return {"lookup_fail": "missing cache", "plate": {}, "success": False}
79
+ try:
80
+ with self.cache_session as client:
81
+ log.debug(f"Fetching DVLA info from API, cache_ttl={self.cache_session.expire_after}")
82
+ response: CachedResponse = cast(
83
+ "CachedResponse",
84
+ client.post(
85
+ url=f"https://{self.env_prefix}driver-vehicle-licensing.api.gov.uk/vehicle-enquiry/v1/vehicles",
86
+ headers={"x-api-key": self.api_key, "Content-Type": "application/json"},
87
+ json={"registrationNumber": reg.upper()},
88
+ ),
89
+ )
90
+ if response.from_cache:
91
+ log.debug("DVLA API cached response, created %s", response.created_at)
92
+ if response.status_code == 200:
93
+ return {
94
+ "cache": {
95
+ "calls": len(response.history) if response.history else 0,
96
+ "cached": response.from_cache,
97
+ "created": response.created_at.isoformat() if response.created_at else None,
98
+ },
99
+ "plate": cast("dict[str,Any]", response.json()),
100
+ "success": True,
101
+ }
102
+
103
+ log.error("DVLA API FAIL: %s", response.json())
104
+ return {
105
+ "api_errors": response.json()["errors"],
106
+ "api_status": response.status_code,
107
+ "plate": {},
108
+ "success": False,
109
+ }
110
+ except Exception as e:
111
+ log.exception("Failed to fetch DVLA reg data")
112
+ return {"api_exception": str(e), "plate": {}, "success": False}