├── mockup.jpg ├── server.js ├── package.json ├── index.html ├── main.css ├── sharenow.js ├── Readme.md └── main.js /mockup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T3rm1/share-now-api/HEAD/mockup.jpg -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const shareNow = require("./sharenow.js"); 2 | const express = require("express"); 3 | const WebSocket = require("ws"); 4 | 5 | const app = express(); 6 | const wss = new WebSocket.Server({port: 8081}); 7 | let client = new shareNow(); 8 | client.connect(); 9 | wss.on("connection", ws => { 10 | ws.send(JSON.stringify(client.getVehicles(vehicleUpdate => { 11 | ws.send(JSON.stringify(vehicleUpdate)); 12 | }))) 13 | }); 14 | 15 | app.use(express.static(__dirname)); 16 | app.get("/", (req, res) => { 17 | res.sendFile("index.html", {root: __dirname}); 18 | }); 19 | 20 | app.listen(8080, () => { 21 | console.log("Server is up and running on port 8080."); 22 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "share-now-api", 3 | "version": "1.0.0", 4 | "description": "Demo project to show how to use the Share Now api anonymously.", 5 | "main": "server.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/T3rm1/share-now-api.git" 9 | }, 10 | "dependencies": { 11 | "express": "^4.17.1", 12 | "mqtt": "^4.0.1", 13 | "uuid-random": "^1.3.0", 14 | "ws": "^7.3.0" 15 | }, 16 | "devDependencies": {}, 17 | "scripts": { 18 | "test": "echo \"Error: no test specified\" && exit 1" 19 | }, 20 | "keywords": [ 21 | "mqtt", 22 | "sharenow" 23 | ], 24 | "author": "T3rm1", 25 | "license": "ISC", 26 | "homepage": "https://github.com/T3rm1/share-now-api" 27 | } 28 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Share Now cars 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0px; 4 | font-family: Verdana, Geneva, Tahoma, sans-serif; 5 | } 6 | 7 | #map { 8 | height: 100%; 9 | } 10 | 11 | nav { 12 | padding: 10px; 13 | background-color: #0ba1e2; 14 | } 15 | 16 | nav ul { 17 | margin: 0px; 18 | display: flex; 19 | justify-content: center; 20 | align-content: center; 21 | list-style: none; 22 | margin: 10px; 23 | } 24 | 25 | nav ul li { 26 | margin-left: 10px; 27 | float: left; 28 | border-radius: 20px; 29 | color: white; 30 | padding: 10px; 31 | cursor: pointer; 32 | } 33 | 34 | nav ul li:hover:not(.selected) { 35 | background-color: rgba(255, 255, 255, 0.3); 36 | } 37 | 38 | li.selected { 39 | border: 2px solid white; 40 | } 41 | 42 | .marker { 43 | width: 2rem; 44 | height: 2rem; 45 | display: block; 46 | left: -1.0rem; 47 | top: -1.0rem; 48 | position: relative; 49 | border-radius: 3rem 3rem 0; 50 | transform: rotate(45deg); 51 | } 52 | .available { 53 | background-color: #0ba1e2; 54 | border: 1px solid #0ba1e2; 55 | } 56 | .unavailable { 57 | background-color: #53595c; 58 | border: 1px solid #53595c; 59 | } 60 | .new { 61 | background-color: #25a133; 62 | border: 1px solid #25a133; 63 | } 64 | .marker i { 65 | transform: rotate(-45deg); 66 | position: relative; 67 | top: 4px; 68 | left: 4px; 69 | font-size: 1.5rem; 70 | color: white; 71 | } 72 | .popup-car { 73 | display: flex; 74 | } 75 | .popup-car img { 76 | object-fit: contain; 77 | } 78 | .popup-car-details { 79 | justify-content: center; 80 | font-size: large; 81 | display: flex; 82 | flex-direction: column; 83 | } -------------------------------------------------------------------------------- /sharenow.js: -------------------------------------------------------------------------------- 1 | const mqtt = require("mqtt"); 2 | const zlip = require("zlib"); 3 | const uuid = require("uuid-random"); 4 | 5 | class ShareNowClient { 6 | static VEHICLELISTDELTA = "C2G/S2C/3/VEHICLELISTDELTA.GZ"; 7 | static VEHICLELIST = "C2G/S2C/3/VEHICLELIST.GZ" 8 | vehicles = []; 9 | #updateCallback; 10 | 11 | connect() { 12 | let clientId = `a:${uuid()}`; 13 | let client = mqtt.connect('mqtts://driver.eu.share-now.com:443', { 14 | clientId, 15 | rejectUnauthorized: false, 16 | reconnectPeriod: 0 17 | }); 18 | 19 | client.on('connect', () => { 20 | console.log("Connected to MQTT broker. Subscribing to topics."); 21 | client.subscribe(ShareNowClient.VEHICLELIST, {qos: 0}); 22 | client.subscribe(ShareNowClient.VEHICLELISTDELTA, {qos: 1}); 23 | }); 24 | 25 | client.on("message", (topic, message) => { 26 | let json = JSON.parse(zlip.gunzipSync(message)); 27 | if (topic === ShareNowClient.VEHICLELISTDELTA) { 28 | this.updateVehicles(json); 29 | if (this.#updateCallback !== undefined) { 30 | this.#updateCallback(json); 31 | } 32 | } else if (topic === ShareNowClient.VEHICLELIST) { 33 | console.log("Received initial vehicle list"); 34 | client.unsubscribe(ShareNowClient.VEHICLELIST); 35 | this.vehicles = json.connectedVehicles; 36 | } 37 | }); 38 | 39 | client.on("error", error => { 40 | console.log(`Error: ${error}`); 41 | }); 42 | 43 | client.on("close", () => { 44 | console.log("Close"); 45 | }); 46 | } 47 | 48 | getVehicles(callback) { 49 | this.#updateCallback = callback; 50 | return this.vehicles; 51 | } 52 | 53 | updateVehicles(vehicleUpdate) { 54 | this.vehicles = this.vehicles.concat(vehicleUpdate.addedVehicles); 55 | vehicleUpdate.removedVehicles.forEach(vehicleId => { 56 | this.vehicles = this.vehicles.filter(e => e.id !== vehicleId); 57 | }); 58 | } 59 | } 60 | 61 | module.exports = ShareNowClient; -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ![Preview of demo website](mockup.jpg) 2 | 3 | # Share Now car locations 4 | Here is [demo page](https://share-now-api.herokuapp.com) that shows the locations of all Share Now cars. Only cars in Hamburg are shown right now. 5 | 6 | Green markers are cars that became available. 7 | 8 | Grey markers are cars that became unavailable. 9 | 10 | ## Usage 11 | ``` 12 | npm install 13 | node server.js 14 | ``` 15 | Go to [http://localhost:8080](http://localhost:8080). 16 | 17 | 18 | ## MQTT 19 | The data is serverd by a MQTT broker. For Europe the endpoint is `mqtts://driver.eu.share-now.com:443`. The clientId is a random UUID prefixed with `a:` for anonymous connections. No username and password are required. 20 | 21 | You can subscribe to multiple topics. The schema is `C2G/S2C//.GZ` where `topic` is either `VEHICLELIST` or `VEHICLELISTDELTA`. 22 | 23 | Possible `locationId` values are: 24 | 25 | - Germany 26 | - Hamburg - `3` 27 | - Berlin - `12` 28 | - Frankfurt am Main - `33` 29 | - München - `26` 30 | - Köln - `19` 31 | - Stuttgart - `18` 32 | - Italy 33 | - Milan - `20` 34 | - Rome - `31` 35 | - Turin - `44` 36 | - Other countries 37 | - Copenhagen - `52` 38 | - Paris - `48` 39 | - Amsterdam - `5` 40 | - Vienna - `7` 41 | - Madrid - `36` 42 | - Budapest - `55` 43 | 44 | The data received is json compressed with gzip. 45 | 46 | ### VEHICLELIST 47 | Subscriptions to this topic will give you a list with all cars for the given location. A message is sent each time there is an update, so you should unsubscribe as soon as you get the first message. 48 | 49 |
50 | Example 51 | 52 | ```json 53 | { 54 | "connectedVehicles": [ 55 | { 56 | "id": "WBY8P210107E82494", 57 | "plate": "M-EV1558E", 58 | "geoCoordinate": { 59 | "latitude": 53.57132, 60 | "longitude": 9.95367 61 | }, 62 | "fuellevel": 84, 63 | "address": "Fruchtallee 107, 20259 Hamburg", 64 | "locationId": "3", 65 | "buildSeries": "BMW_I3", 66 | "fuelType": "ELECTRIC", 67 | "primaryColor": "B85U", 68 | "hardwareVersion": "HW42", 69 | "imageUrl": "https://www.car2go.com/rentalassets/vehicles/{density}/bmw_i3_capparis_white.png", 70 | "transmission": "GA", 71 | "rank": 1, 72 | "vin": "WBY8P210107E82494", 73 | "locationIdAsLong": 3 74 | } 75 | ], 76 | "locationId": 3, 77 | "eventType": "CONNECTED_VEHICLES", 78 | "timestamp": 1589666154706 79 | } 80 | ``` 81 |
82 | 83 | ### VEHICLELISTDELTA 84 | This topic receives messages whenever a car becomes available or unavailable. 85 | 86 |
87 | Example 88 | 89 | ```json 90 | { 91 | "addedVehicles": [ 92 | { 93 | "id": "WME4533421K323858", 94 | "plate": "HH-GO8560", 95 | "geoCoordinate": { 96 | "latitude": 53.55533, 97 | "longitude": 10.02782 98 | }, 99 | "fuellevel": 59, 100 | "address": "Jungestra\u00c3\u0178e 6, 20535 Hamburg", 101 | "locationId": "3", 102 | "buildSeries": "C453", 103 | "fuelType": "GASOLINE", 104 | "primaryColor": "EN2U", 105 | "secondaryColor": "EDAO", 106 | "hardwareVersion": "HW3", 107 | "imageUrl": "https://www.car2go.com/rentalassets/vehicles/{density}/c453_silver.png", 108 | "transmission": "GA", 109 | "rank": 1, 110 | "vin": "WME4533421K323858", 111 | "locationIdAsLong": 3 112 | } 113 | ], 114 | "removedVehicles": [ 115 | "WME4533421K291769" 116 | ], 117 | "locationId": 3, 118 | "timestamp": 1589656739007, 119 | "eventType": "VEHICLE_LIST_UPDATE" 120 | } 121 | ``` 122 |
123 | 124 | ### Code 125 | All Share Now api related code is in [sharenow.js](sharenow.js). Rest of the files are for the demo webpage. 126 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const locations = { 2 | "Germany": [ 3 | {name : "Hamburg", id: 3, geo: {lat: 53.57132, lng: 9.95367}}, 4 | {name : "Berlin", id: 12, geo: {lat: 52.5069704, lng: 13.2846517}}, 5 | {name : "Frankfurt am Main", id: 33, geo: {lat: 50.121301, lng: 8.5665248}}, 6 | {name : "München", id: 26, geo: {lat: 48.155004, lng: 11.4717967}}, 7 | {name : "Köln", id: 19, geo: {lat: 50.95779, lng: 6.8972834}}, 8 | {name : "Stuttgart", id: 18, geo: {lat: 48.779301, lng: 9.1071762}} 9 | ], 10 | "Denmark": [ 11 | {name : "Kopenhagen", id: 52, geo: {lat: 55.6713108, lng: 12.5588047}} 12 | ], 13 | "France": [ 14 | {name : "Paris", id: 48, geo: {lat: 48.8589101, lng: 2.3120407}} 15 | ], 16 | "Italy": [ 17 | {name : "Mailand", id: 20, geo: {lat: 45.4627887, lng: 9.142713}}, 18 | {name : "Rom", id: 31, geo: {lat: 41.9101776, lng: 12.4659587}}, 19 | {name : "Turin", id: 44, geo: {lat: 45.073544, lng: 7.6405873}} 20 | ], 21 | "Netherlands": [ 22 | {name : "Amsterdam", id: 5, geo: {lat: 52.3547498, lng: 4.8339214}} 23 | ], 24 | "Austria": [ 25 | {name : "Wien", id: 7, geo: {lat: 48.220778, lng: 16.3100209}} 26 | ], 27 | "Spain": [ 28 | {name : "Madrid", id: 36, geo: {lat: 40.4380638, lng: -3.7495758}} 29 | ], 30 | "Hungary": [ 31 | {name : "Budapest", id: 55, geo: {lat: 47.4813081, lng: 19.0602639}} 32 | ] 33 | } 34 | const idToCity = {}; 35 | Object.keys(locations).forEach(country => locations[country].forEach(city => idToCity[city.id] = city)); 36 | 37 | document.querySelectorAll("#countries li").forEach(li => { 38 | li.onclick = () => { 39 | updateCities(li.innerText); 40 | document.querySelectorAll("#countries li.selected").forEach(li => li.classList.remove("selected")); 41 | li.classList.add("selected"); 42 | } 43 | }); 44 | registerCityClickListeners(); 45 | function registerCityClickListeners() { 46 | document.querySelectorAll("#cities li").forEach(li => { 47 | li.onclick = () => { 48 | changeLocation(li.dataset.id); 49 | } 50 | }); 51 | } 52 | 53 | function updateCities(country) { 54 | ul = document.getElementById("cities"); 55 | ul.innerHTML = ""; 56 | locations[country].forEach(city => { 57 | ul.insertAdjacentHTML("beforeend", `
  • ${city.name}
  • `) 58 | }); 59 | registerCityClickListeners(); 60 | } 61 | function changeLocation(id) { 62 | let city = idToCity[id]; 63 | map.setView([city.geo.lat, city.geo.lng], 13); 64 | } 65 | 66 | var map = L.map('map').setView([53.57132, 9.95367], 13); 67 | L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 68 | attribution: '© OpenStreetMap contributors' 69 | }).addTo(map); 70 | 71 | let markers = {}; 72 | function addVehicleMarker(vehicle, initial = false) { 73 | let geoCoords = vehicle.geoCoordinate; 74 | if (geoCoords !== undefined) { 75 | const icon = L.divIcon({ 76 | iconAnchor: [0, 24], 77 | labelAnchor: [-6, 0], 78 | popupAnchor: [0, -36], 79 | html: `` 80 | }); 81 | let fuelIcon = vehicle.fuelType === "ELECTRIC" ? '' : ''; 82 | let marker = L.marker([vehicle.geoCoordinate.latitude, vehicle.geoCoordinate.longitude]) 83 | .addTo(map) 84 | .setIcon(icon) 85 | .bindPopup(` 86 | 99 | `); 100 | markers[vehicle.id] = marker; 101 | } 102 | } 103 | 104 | function removeVehicleMarker(id) { 105 | let marker = markers[id]; 106 | if (marker !== undefined) { 107 | //marker.removeFrom(map); 108 | //delete markers.id; 109 | const icon = L.divIcon({ 110 | iconAnchor: [0, 24], 111 | labelAnchor: [-6, 0], 112 | popupAnchor: [0, -36], 113 | html: `` 114 | }); 115 | marker.setIcon(icon); 116 | } 117 | } 118 | 119 | ws = new WebSocket("ws://localhost:8081"); 120 | ws.onmessage = event => { 121 | json = JSON.parse(event.data); 122 | if (Array.isArray(json)) { 123 | // initial list 124 | json.forEach(v => addVehicleMarker(v, true)); 125 | } else if ("VEHICLE_LIST_UPDATE" === json.eventType) { 126 | json.addedVehicles.forEach(addVehicleMarker); 127 | json.removedVehicles.forEach(removeVehicleMarker); 128 | } 129 | }; 130 | --------------------------------------------------------------------------------