diff --git a/bosch-tps110/CHANGELOG.md b/bosch-tps110/CHANGELOG.md new file mode 100644 index 00000000..dda6ac59 --- /dev/null +++ b/bosch-tps110/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 1.0.0 - 2026-01-15 + +- First version of plugin \ No newline at end of file diff --git a/bosch-tps110/LICENSE.md b/bosch-tps110/LICENSE.md new file mode 100644 index 00000000..bffeef34 --- /dev/null +++ b/bosch-tps110/LICENSE.md @@ -0,0 +1,7 @@ +Copyright 2026 Thinger.io + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/bosch-tps110/README.md b/bosch-tps110/README.md new file mode 100644 index 00000000..261a29f3 --- /dev/null +++ b/bosch-tps110/README.md @@ -0,0 +1,71 @@ +Based on the device data provided, I will create the README.md documentation: + +```markdown +# BOSCH Parking Lot Sensor + +The BOSCH wireless smart parking sensor is a LoRaWAN® end device designed for parking space occupancy detection. It utilizes dual-sensor technology combining a magnetometer and radar for highly reliable vehicle detection. The sensor enables active parking lot management features including search, navigation, and reservation capabilities. Additionally, the BOSCH parking lot sensor integrates a GPS receiver for precise location tracking and a temperature sensor for environmental monitoring. + +## Key Features + +- **Dual-sensor detection**: Magnetometer and radar for accurate occupancy detection +- **GPS receiver**: Precise geolocation of parking spaces +- **Temperature sensor**: Environmental monitoring capability +- **LoRaWAN® connectivity**: Long-range, low-power wireless communication +- **Robust design**: Suitable for outdoor parking lot installations + +## Sensors and Measurements + +| Sensor | Measurement | Description | +|--------|-------------|-------------| +| Magnetometer | Magnetic field variation | Detects vehicle presence by measuring magnetic field changes | +| Radar | Object detection | Provides secondary occupancy verification | +| GPS | Latitude/Longitude | Enables precise parking space positioning | +| Temperature | °C | Monitors ambient temperature conditions | + +## Use Cases + +- Smart city parking management +- Shopping center and retail parking guidance +- Airport and transportation hub parking systems +- Street parking monitoring +- Commercial and industrial parking facilities +- Parking search and navigation applications +- Parking space reservation systems + +## Thinger.io Integration + +### Requirements + +A LoRaWAN server is required to connect the BOSCH Parking Lot Sensor to Thinger.io. Supported options include: + +- [The Things Stack](https://www.thethingsindustries.com/stack/) +- [LORIOT](https://loriot.io/) +- [ChirpStack](https://www.chirpstack.io/) + +The corresponding plugin for the selected LoRaWAN server must be installed in your Thinger.io instance. + +### Get Started + +#### Installation + +Look for the plugin in the [Thinger.io Plugin Store](https://marketplace.thinger.io/) and install it in your Thinger.io instance. Once the plugin is installed, a new Product will be created for this device. + +#### Configuration + +The Product is preconfigured for immediate use. Verify that the auto provision prefix matches the one selected in your LoRaWAN server plugin in Thinger.io, or adjust it as needed. + +#### Usage + +Start sending uplinks for automatic device and bucket provisioning. + +This product provides: +- Parking occupancy status tracking +- GPS position data storage +- Temperature monitoring +- Predefined dashboard for visualization + +## Additional Resources + +- [BOSCH Connectivity](https://www.bosch-connectivity.com/) +- [Thinger.io Documentation](https://docs.thinger.io) +``` \ No newline at end of file diff --git a/bosch-tps110/assets/bosch-tps110.png b/bosch-tps110/assets/bosch-tps110.png new file mode 100644 index 00000000..e69de29b diff --git a/bosch-tps110/plugin.json b/bosch-tps110/plugin.json new file mode 100644 index 00000000..82175305 --- /dev/null +++ b/bosch-tps110/plugin.json @@ -0,0 +1,599 @@ +{ + "name": "bosch_tps110", + "version": "1.0.0", + "description": "BOSCH wireless smart parking sensor with magnetometer, radar, GPS, and temperature sensors for parking lot management", + "author": "Thinger.io", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/thinger-io/plugins.git", + "directory": "bosch-tps110" + }, + "metadata": { + "name": "BOSCH TPS110", + "description": "BOSCH wireless smart parking sensor with magnetometer, radar, GPS, and temperature sensors for parking lot management", + "image": "assets/tps110.png", + "category": "devices", + "vendor": "bosch" + }, + "resources": { + "products": [ + { + "description": "BOSCH wireless smart parking sensor with magnetometer, radar, GPS, and temperature sensors for parking lot management", + "enabled": true, + "name": "BOSCH TPS110", + "product": "bosch_tps110", + "profile": { + "api": { + "downlink": { + "enabled": true, + "handle_connectivity": false, + "request": { + "data": { + "path": "/downlink", + "payload": "{\n \"data\" : \"{{payload.data=\"\"}}\",\n \"port\" : {{payload.port=85}},\n \"priority\": {{payload.priority=3}},\n \"confirmed\" : {{payload.confirmed=false}},\n \"uplink\" : {{property.uplink}} \n}", + "payload_function": "", + "payload_type": "source_payload", + "plugin": "{{property.uplink.source}}", + "target": "plugin_endpoint" + } + } + }, + "uplink": { + "device_id_resolver": "getId", + "enabled": true, + "handle_connectivity": true, + "request": { + "data": { + "payload": "{{payload}}", + "payload_function": "", + "payload_type": "source_payload", + "resource_stream": "uplink", + "target": "resource_stream" + } + } + } + }, + "autoprovisions": { + "bosch_tps110_autoprovision": { + "config": { + "mode": "pattern", + "pattern": "bosch-tps110-.*" + }, + "enabled": true + } + }, + "buckets": { + "bosch_tps110_data_bucket": { + "backend": "mongodb", + "data": { + "payload": "{{payload}}", + "payload_function": "decodeThingerUplink", + "payload_type": "source_payload", + "resource": "uplink", + "source": "resource", + "update": "events" + }, + "enabled": true, + "retention": { + "period": 6, + "unit": "months" + }, + "tags": [] + } + }, + "code": { + "code": "function decodeThingerUplink(thingerData) {\n // 0. If data has already been decoded, we will return it\n if (thingerData.decodedPayload) return thingerData.decodedPayload;\n \n // 1. Extract and Validate Input\n // We need 'payload' (hex string) and 'fPort' (integer)\n const hexPayload = thingerData.payload || \"\";\n const port = thingerData.fPort || 1;\n\n // 2. Convert Hex String to Byte Array\n const bytes = [];\n for (let i = 0; i < hexPayload.length; i += 2) {\n bytes.push(parseInt(hexPayload.substr(i, 2), 16));\n }\n\n // 3. Dynamic Function Detection and Execution\n \n // CASE A: (The Things Stack v3)\n if (typeof decodeUplink === 'function') {\n try {\n const input = {\n bytes: bytes,\n fPort: port\n };\n var result = decodeUplink(input);\n \n if (result.data) return result.data;\n\n return result; \n } catch (e) {\n console.error(\"Error inside decodeUplink:\", e);\n throw e;\n }\n }\n\n // CASE B: Legacy TTN (v2)\n else if (typeof Decoder === 'function') {\n try {\n return Decoder(bytes, port);\n } catch (e) {\n console.error(\"Error inside Decoder:\", e);\n throw e;\n }\n }\n\n // CASE C: No decoder found\n else {\n throw new Error(\"No compatible TTN decoder function (decodeUplink or Decoder) found in scope.\");\n }\n}\n\n\n// TTN decoder\nfunction decodeUplink(input) {\n var data = {};\n switch (input.fPort) {\n case 1: // Parking status\n data.type = 'parking status';\n data.occupied = (input.bytes[0] & 0x1) === 0x1;\n break;\n\n case 2: // Heartbeat\n data.type = 'heartbeat';\n data.occupied = (input.bytes[0] & 0x1) === 0x1;\n if (input.bytes.length >= 2) {\n data.temperature =\n input.bytes[1] & 0x80 ? input.bytes[1] - 0x100 : input.bytes[1];\n }\n break;\n\n case 3: // Start-up\n data.type = 'startup';\n data.debugCodes = [];\n for (var i = 0; i <= 8; i += 4) {\n var debugCode = ((input.bytes[i + 1] & 0xf) << 8) | input.bytes[i];\n if (debugCode) {\n data.debugCodes.push(debugCode);\n }\n }\n data.firmwareVersion =\n input.bytes[12] + '.' + input.bytes[13] + '.' + input.bytes[14];\n data.resetCause = [\n undefined,\n 'watchdog',\n 'power on',\n 'system request',\n 'other',\n ][input.bytes[15]];\n data.occupied = (input.bytes[16] & 0x1) == 0x1;\n break;\n\n case 4: // Device information\n data.type = 'device information';\n data.bytes = input.bytes;\n break;\n\n case 5: // Device usage\n data.type = 'device usage';\n data.bytes = input.bytes;\n break;\n\n case 6: // Debug\n data.type = 'debug';\n data.timestamp =\n (input.bytes[3] << 24) |\n (input.bytes[2] << 16) |\n (input.bytes[1] << 8) |\n input.bytes[0];\n data.debugCode = ((input.bytes[5] & 0xf) << 8) | input.bytes[4];\n data.sequenceNumber = (input.bytes[9] << 8) | input.bytes[8];\n break;\n }\n\n return {\n data: data,\n };\n}\n", + "environment": "javascript", + "storage": "", + "version": "1.0" + }, + "properties": { + "bosch_tps110_location": { + "data": { + "payload": "{\"latitude\": {{payload.latitude}}, \"longitude\": {{payload.longitude}}}", + "payload_function": "decodeThingerUplink", + "payload_type": "source_payload", + "resource": "uplink", + "source": "resource", + "update": "events" + }, + "default": { + "latitude": 0, + "longitude": 0 + }, + "enabled": true + }, + "bosch_tps110_occupancy": { + "data": { + "payload": "{{payload.occupied}}", + "payload_function": "decodeThingerUplink", + "payload_type": "source_payload", + "resource": "uplink", + "source": "resource", + "update": "events" + }, + "default": { + "occupied": false + }, + "enabled": true + }, + "uplink": { + "data": { + "payload": "{{payload}}", + "payload_function": "", + "payload_type": "source_payload", + "resource": "uplink", + "source": "resource", + "update": "events" + }, + "default": { + "source": "value" + }, + "enabled": true + } + } + }, + "_resources": { + "properties": [ + { + "property": "dashboard", + "value": { + "tabs": [ + { + "name": "Parking Monitor", + "widgets": [ + { + "layout": { + "col": 0, + "row": 0, + "sizeX": 3, + "sizeY": 6 + }, + "panel": { + "color": "#1976D2", + "currentColor": "#1976D2", + "showOffline": { + "type": "last_sample" + }, + "title": "Parking Status" + }, + "properties": { + "color": "#4CAF50", + "decimal_places": 0, + "icon": "fa fa-car", + "text": "Occupied", + "unit": "" + }, + "sources": [ + { + "bucket": { + "backend": "mongodb", + "id": "bosch_tps110_data_bucket", + "mapping": "occupied", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#4CAF50", + "name": "Occupancy", + "source": "bucket", + "timespan": { + "mode": "latest" + } + } + ], + "type": "text" + }, + { + "layout": { + "col": 3, + "row": 0, + "sizeX": 3, + "sizeY": 6 + }, + "panel": { + "color": "#FF9800", + "currentColor": "#FF9800", + "showOffline": { + "type": "none" + }, + "title": "Temperature" + }, + "properties": { + "color": "#FF5722", + "max": 50, + "min": -20, + "unit": "°C" + }, + "sources": [ + { + "bucket": { + "backend": "mongodb", + "id": "bosch_tps110_data_bucket", + "mapping": "temperature", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#FF5722", + "name": "Temperature", + "source": "bucket", + "timespan": { + "mode": "latest" + } + } + ], + "type": "donutchart" + }, + { + "layout": { + "col": 6, + "row": 0, + "sizeX": 3, + "sizeY": 6 + }, + "panel": { + "color": "#FFC107", + "currentColor": "#FFC107", + "showOffline": { + "type": "none" + }, + "title": "Battery" + }, + "properties": { + "color": "#4CAF50", + "max": 4.2, + "min": 2.5, + "unit": "V" + }, + "sources": [ + { + "bucket": { + "backend": "mongodb", + "id": "bosch_tps110_data_bucket", + "mapping": "battery", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#4CAF50", + "name": "Battery Voltage", + "source": "bucket", + "timespan": { + "mode": "latest" + } + } + ], + "type": "donutchart" + }, + { + "layout": { + "col": 9, + "row": 0, + "sizeX": 3, + "sizeY": 6 + }, + "panel": { + "color": "#9C27B0", + "currentColor": "#9C27B0", + "showOffline": { + "type": "none" + }, + "title": "Radar Confidence" + }, + "properties": { + "color": "#E91E63", + "max": 100, + "min": 0, + "unit": "%" + }, + "sources": [ + { + "bucket": { + "backend": "mongodb", + "id": "bosch_tps110_data_bucket", + "mapping": "radar_confidence", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#E91E63", + "name": "Confidence", + "source": "bucket", + "timespan": { + "mode": "latest" + } + } + ], + "type": "donutchart" + }, + { + "layout": { + "col": 0, + "row": 6, + "sizeX": 6, + "sizeY": 12 + }, + "panel": { + "color": "#424242", + "currentColor": "#424242", + "showFullscreen": true, + "showOffline": { + "type": "none" + }, + "subtitle": "Last 7 days", + "title": "Occupancy History" + }, + "properties": { + "alignTimeSeries": true, + "dataAppend": false, + "options": "var options = {\n series: series,\n chart: {\n type: 'area',\n background: '#424242',\n toolbar: {\n show: true,\n tools: {\n download: true,\n selection: true,\n zoom: true,\n zoomin: true,\n zoomout: true,\n pan: true,\n reset: true\n },\n autoSelected: 'zoom'\n },\n zoom: {\n enabled: true,\n type: 'x',\n autoScaleYaxis: true\n }\n },\n stroke: {\n curve: 'stepline',\n width: 2\n },\n fill: {\n type: 'solid',\n opacity: 0.6\n },\n grid: {\n row: {\n colors: ['#555555', 'transparent'],\n opacity: 0.3\n }\n },\n xaxis: {\n type: 'datetime',\n labels: {\n datetimeUTC: false,\n style: {\n colors: '#FFFFFF'\n }\n }\n },\n yaxis: {\n min: 0,\n max: 1,\n tickAmount: 1,\n labels: {\n formatter: function(val) {\n return val === 1 ? 'Occupied' : 'Free';\n },\n style: {\n colors: '#FFFFFF'\n }\n }\n },\n tooltip: {\n x: {\n format: 'dd/MM/yyyy HH:mm:ss'\n },\n y: {\n formatter: function(val) {\n return val === 1 ? 'Occupied' : 'Free';\n }\n },\n shared: true\n },\n legend: {\n position: 'bottom',\n labels: {\n colors: '#FFFFFF'\n }\n },\n colors: ['#4CAF50']\n};", + "realTimeUpdate": false + }, + "sources": [ + { + "aggregation": { + "period": "hour", + "type": "avg" + }, + "bucket": { + "backend": "mongodb", + "id": "bosch_tps110_data_bucket", + "mapping": "occupied", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#4CAF50", + "name": "Parking Space", + "source": "bucket", + "timespan": { + "magnitude": "day", + "mode": "relative", + "period": "latest", + "value": 7 + }, + "transform": "" + } + ], + "type": "apex_charts" + }, + { + "layout": { + "col": 6, + "row": 6, + "sizeX": 6, + "sizeY": 12 + }, + "panel": { + "color": "#1565C0", + "currentColor": "#1565C0", + "showFullscreen": true, + "showOffline": { + "type": "none" + }, + "subtitle": "Temperature trends", + "title": "Environmental Monitoring" + }, + "properties": { + "alignTimeSeries": true, + "dataAppend": false, + "options": "var options = {\n series: series,\n chart: {\n background: '#1565C0',\n toolbar: {\n show: true,\n autoSelected: 'zoom'\n },\n zoom: {\n enabled: true,\n type: 'x',\n autoScaleYaxis: true\n }\n },\n stroke: {\n curve: 'smooth',\n width: 2\n },\n grid: {\n row: {\n colors: ['#1976D2', 'transparent'],\n opacity: 0.3\n }\n },\n xaxis: {\n type: 'datetime',\n labels: {\n datetimeUTC: false,\n style: {\n colors: '#FFFFFF'\n }\n }\n },\n yaxis: {\n labels: {\n formatter: function(val) {\n return val.toFixed(1) + '°C';\n },\n style: {\n colors: '#FFFFFF'\n }\n }\n },\n tooltip: {\n x: {\n format: 'dd/MM/yyyy HH:mm'\n },\n shared: true\n },\n legend: {\n position: 'bottom',\n labels: {\n colors: '#FFFFFF'\n }\n },\n colors: ['#FF5722']\n};", + "realTimeUpdate": false + }, + "sources": [ + { + "aggregation": { + "period": "hour", + "type": "avg" + }, + "bucket": { + "backend": "mongodb", + "id": "bosch_tps110_data_bucket", + "mapping": "temperature", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#FF5722", + "name": "Temperature", + "source": "bucket", + "timespan": { + "magnitude": "day", + "mode": "relative", + "period": "latest", + "value": 7 + }, + "transform": "" + } + ], + "type": "apex_charts" + }, + { + "layout": { + "col": 0, + "row": 18, + "sizeX": 12, + "sizeY": 10 + }, + "panel": { + "color": "#ffffff", + "currentColor": "#ffffff", + "showOffline": { + "type": "none" + }, + "title": "Recent Events" + }, + "properties": { + "source": "code", + "template": "
| Timestamp | \nStatus | \nTemperature (°C) | \nBattery (V) | \nRadar Confidence (%) | \nGPS | \n
|---|---|---|---|---|---|
| {{ entry.ts | date:'medium' }} | \n{{ entry.occupied ? 'Occupied' : 'Free' }} | \n{{ entry.temperature || '—' }} | \n{{ entry.battery || '—' }} | \n{{ entry.radar_confidence || '—' }} | \n{{ entry.latitude && entry.longitude ? (entry.latitude + ', ' + entry.longitude) : '—' }} | \n