diff --git a/_ci/apikeys-ci.xml b/_ci/apikeys-ci.xml
index a2b0289cb..9da033c4c 100644
--- a/_ci/apikeys-ci.xml
+++ b/_ci/apikeys-ci.xml
@@ -8,4 +8,5 @@
ci
ci
ci:ci
+ ci
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 94c7857e9..7a0533ab5 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -129,6 +129,17 @@ android {
// add API keys from environment variable if not set in apikeys.xml
applicationVariants.all {
+ var evmapKey =
+ System.getenv("EVMAP_API_KEY") ?: project.findProperty("EVMAP_API_KEY")?.toString()
+ if (evmapKey == null && project.hasProperty("EVMAP_API_KEY_ENCRYPTED")) {
+ evmapKey = decode(
+ project.findProperty("EVMAP_API_KEY_ENCRYPTED").toString(),
+ "FmK.d,-f*p+rD+WK!eds"
+ )
+ }
+ if (evmapKey != null) {
+ resValue("string", "evmap_key", evmapKey)
+ }
val goingelectricKey =
System.getenv("GOINGELECTRIC_API_KEY") ?: project.findProperty("GOINGELECTRIC_API_KEY")
?.toString()
diff --git a/app/schemas/net.vonforst.evmap.storage.AppDatabase/28.json b/app/schemas/net.vonforst.evmap.storage.AppDatabase/28.json
new file mode 100644
index 000000000..49659ad77
--- /dev/null
+++ b/app/schemas/net.vonforst.evmap.storage.AppDatabase/28.json
@@ -0,0 +1,938 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 28,
+ "identityHash": "84f71cce385c444726ba336834ddf6b4",
+ "entities": [
+ {
+ "tableName": "ChargeLocation",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `name` TEXT NOT NULL, `coordinates` BLOB NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `dataSourceUrl` TEXT NOT NULL, `url` TEXT, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `accessibility` TEXT, `license` TEXT, `networkUrl` TEXT, `chargerUrl` TEXT, `timeRetrieved` INTEGER NOT NULL, `isDetailed` INTEGER NOT NULL, `coordinatesProjected` BLOB NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `fault_report_created` INTEGER, `fault_report_description` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, `chargepricecountry` TEXT, `chargepricenetwork` TEXT, `chargepriceplugTypes` TEXT, PRIMARY KEY(`id`, `dataSource`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dataSource",
+ "columnName": "dataSource",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "coordinates",
+ "columnName": "coordinates",
+ "affinity": "BLOB",
+ "notNull": true
+ },
+ {
+ "fieldPath": "chargepoints",
+ "columnName": "chargepoints",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "network",
+ "columnName": "network",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "dataSourceUrl",
+ "columnName": "dataSourceUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "editUrl",
+ "columnName": "editUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "verified",
+ "columnName": "verified",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "barrierFree",
+ "columnName": "barrierFree",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "operator",
+ "columnName": "operator",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "generalInformation",
+ "columnName": "generalInformation",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "amenities",
+ "columnName": "amenities",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "locationDescription",
+ "columnName": "locationDescription",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "photos",
+ "columnName": "photos",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "chargecards",
+ "columnName": "chargecards",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "accessibility",
+ "columnName": "accessibility",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "license",
+ "columnName": "license",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "networkUrl",
+ "columnName": "networkUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "chargerUrl",
+ "columnName": "chargerUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "timeRetrieved",
+ "columnName": "timeRetrieved",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isDetailed",
+ "columnName": "isDetailed",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "coordinatesProjected",
+ "columnName": "coordinatesProjected",
+ "affinity": "BLOB",
+ "notNull": true
+ },
+ {
+ "fieldPath": "address.city",
+ "columnName": "city",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "address.country",
+ "columnName": "country",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "address.postcode",
+ "columnName": "postcode",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "address.street",
+ "columnName": "street",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "faultReport.created",
+ "columnName": "fault_report_created",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "faultReport.description",
+ "columnName": "fault_report_description",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "openinghours.twentyfourSeven",
+ "columnName": "twentyfourSeven",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "openinghours.description",
+ "columnName": "description",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "openinghours.days.monday.start",
+ "columnName": "mostart",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "openinghours.days.monday.end",
+ "columnName": "moend",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "openinghours.days.tuesday.start",
+ "columnName": "tustart",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "openinghours.days.tuesday.end",
+ "columnName": "tuend",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "openinghours.days.wednesday.start",
+ "columnName": "westart",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "openinghours.days.wednesday.end",
+ "columnName": "weend",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "openinghours.days.thursday.start",
+ "columnName": "thstart",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "openinghours.days.thursday.end",
+ "columnName": "thend",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "openinghours.days.friday.start",
+ "columnName": "frstart",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "openinghours.days.friday.end",
+ "columnName": "frend",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "openinghours.days.saturday.start",
+ "columnName": "sastart",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "openinghours.days.saturday.end",
+ "columnName": "saend",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "openinghours.days.sunday.start",
+ "columnName": "sustart",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "openinghours.days.sunday.end",
+ "columnName": "suend",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "openinghours.days.holiday.start",
+ "columnName": "hostart",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "openinghours.days.holiday.end",
+ "columnName": "hoend",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "cost.freecharging",
+ "columnName": "freecharging",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "cost.freeparking",
+ "columnName": "freeparking",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "cost.descriptionShort",
+ "columnName": "descriptionShort",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "cost.descriptionLong",
+ "columnName": "descriptionLong",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "chargepriceData.country",
+ "columnName": "chargepricecountry",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "chargepriceData.network",
+ "columnName": "chargepricenetwork",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "chargepriceData.plugTypes",
+ "columnName": "chargepriceplugTypes",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id",
+ "dataSource"
+ ]
+ }
+ },
+ {
+ "tableName": "Favorite",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
+ "fields": [
+ {
+ "fieldPath": "favoriteId",
+ "columnName": "favoriteId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "chargerId",
+ "columnName": "chargerId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "chargerDataSource",
+ "columnName": "chargerDataSource",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "favoriteId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_Favorite_chargerId_chargerDataSource",
+ "unique": false,
+ "columnNames": [
+ "chargerId",
+ "chargerDataSource"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_Favorite_chargerId_chargerDataSource` ON `${TABLE_NAME}` (`chargerId`, `chargerDataSource`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "ChargeLocation",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "chargerId",
+ "chargerDataSource"
+ ],
+ "referencedColumns": [
+ "id",
+ "dataSource"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "BooleanFilterValue",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dataSource",
+ "columnName": "dataSource",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profile",
+ "columnName": "profile",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "key",
+ "profile",
+ "dataSource"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_BooleanFilterValue_profile_dataSource",
+ "unique": false,
+ "columnNames": [
+ "profile",
+ "dataSource"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_BooleanFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "FilterProfile",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "profile",
+ "dataSource"
+ ],
+ "referencedColumns": [
+ "id",
+ "dataSource"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "MultipleChoiceFilterValue",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `values` TEXT NOT NULL, `all` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "values",
+ "columnName": "values",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "all",
+ "columnName": "all",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dataSource",
+ "columnName": "dataSource",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profile",
+ "columnName": "profile",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "key",
+ "profile",
+ "dataSource"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_MultipleChoiceFilterValue_profile_dataSource",
+ "unique": false,
+ "columnNames": [
+ "profile",
+ "dataSource"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_MultipleChoiceFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "FilterProfile",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "profile",
+ "dataSource"
+ ],
+ "referencedColumns": [
+ "id",
+ "dataSource"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "SliderFilterValue",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dataSource",
+ "columnName": "dataSource",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profile",
+ "columnName": "profile",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "key",
+ "profile",
+ "dataSource"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_SliderFilterValue_profile_dataSource",
+ "unique": false,
+ "columnNames": [
+ "profile",
+ "dataSource"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_SliderFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "FilterProfile",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "profile",
+ "dataSource"
+ ],
+ "referencedColumns": [
+ "id",
+ "dataSource"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "FilterProfile",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `id` INTEGER NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`dataSource`, `id`))",
+ "fields": [
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dataSource",
+ "columnName": "dataSource",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "dataSource",
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_FilterProfile_dataSource_name",
+ "unique": true,
+ "columnNames": [
+ "dataSource",
+ "name"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_FilterProfile_dataSource_name` ON `${TABLE_NAME}` (`dataSource`, `name`)"
+ }
+ ]
+ },
+ {
+ "tableName": "RecentAutocompletePlace",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `primaryText` TEXT NOT NULL, `secondaryText` TEXT NOT NULL, `latLng` TEXT NOT NULL, `viewport` TEXT, `types` TEXT NOT NULL, PRIMARY KEY(`id`, `dataSource`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dataSource",
+ "columnName": "dataSource",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "primaryText",
+ "columnName": "primaryText",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "secondaryText",
+ "columnName": "secondaryText",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "latLng",
+ "columnName": "latLng",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "viewport",
+ "columnName": "viewport",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "types",
+ "columnName": "types",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id",
+ "dataSource"
+ ]
+ }
+ },
+ {
+ "tableName": "GEPlug",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
+ "fields": [
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "name"
+ ]
+ }
+ },
+ {
+ "tableName": "GENetwork",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
+ "fields": [
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "name"
+ ]
+ }
+ },
+ {
+ "tableName": "GEChargeCard",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "OCMConnectionType",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `formalName` TEXT, `discontinued` INTEGER, `obsolete` INTEGER, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "formalName",
+ "columnName": "formalName",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "discontinued",
+ "columnName": "discontinued",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "obsolete",
+ "columnName": "obsolete",
+ "affinity": "INTEGER"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "OCMCountry",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isoCode` TEXT NOT NULL, `continentCode` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isoCode",
+ "columnName": "isoCode",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "continentCode",
+ "columnName": "continentCode",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "OCMOperator",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `websiteUrl` TEXT, `title` TEXT NOT NULL, `contactEmail` TEXT, `contactTelephone1` TEXT, `contactTelephone2` TEXT, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "websiteUrl",
+ "columnName": "websiteUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contactEmail",
+ "columnName": "contactEmail",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "contactTelephone1",
+ "columnName": "contactTelephone1",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "contactTelephone2",
+ "columnName": "contactTelephone2",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "OSMNetwork",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
+ "fields": [
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "name"
+ ]
+ }
+ },
+ {
+ "tableName": "SavedRegion",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`region` BLOB NOT NULL, `dataSource` TEXT NOT NULL, `timeRetrieved` INTEGER NOT NULL, `filters` TEXT, `isDetailed` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
+ "fields": [
+ {
+ "fieldPath": "region",
+ "columnName": "region",
+ "affinity": "BLOB",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dataSource",
+ "columnName": "dataSource",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timeRetrieved",
+ "columnName": "timeRetrieved",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "filters",
+ "columnName": "filters",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "isDetailed",
+ "columnName": "isDetailed",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_SavedRegion_filters_dataSource",
+ "unique": false,
+ "columnNames": [
+ "filters",
+ "dataSource"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_SavedRegion_filters_dataSource` ON `${TABLE_NAME}` (`filters`, `dataSource`)"
+ }
+ ]
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '84f71cce385c444726ba336834ddf6b4')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt
index 021dd56d7..fa7bd3b3c 100644
--- a/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt
+++ b/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt
@@ -175,6 +175,7 @@ class AvailabilityRepository(context: Context) {
RheinenergieAvailabilityDetector(okhttp),
teslaOwnerAvailabilityDetector,
TeslaGuestAvailabilityDetector(okhttp),
+ NobilAvailabilityDetector(okhttp, context),
EnBwAvailabilityDetector(okhttp),
NewMotionAvailabilityDetector(okhttp)
)
diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/NobilAvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/NobilAvailabilityDetector.kt
new file mode 100644
index 000000000..c8d3794e7
--- /dev/null
+++ b/app/src/main/java/net/vonforst/evmap/api/availability/NobilAvailabilityDetector.kt
@@ -0,0 +1,109 @@
+package net.vonforst.evmap.api.availability
+
+import android.content.Context
+import com.squareup.moshi.FromJson
+import com.squareup.moshi.JsonClass
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.ToJson
+import net.vonforst.evmap.R
+import net.vonforst.evmap.model.ChargeLocation
+import okhttp3.OkHttpClient
+import retrofit2.Retrofit
+import retrofit2.converter.moshi.MoshiConverterFactory
+import retrofit2.http.GET
+import retrofit2.http.Header
+import retrofit2.http.Path
+import java.time.Instant
+
+internal class InstantStringAdapter {
+ @FromJson
+ fun fromJson(value: String?): Instant? = value?.let {
+ Instant.parse(value)
+ }
+
+ @ToJson
+ fun toJson(value: Instant?): String? = value?.toString()
+}
+
+interface NobilRealtimeApi {
+ @GET("{nobilId}")
+ suspend fun getAvailability(
+ @Path("nobilId") nobilId: String,
+ @Header("X-Api-Key") apiKey: String
+ ): List
+
+ companion object {
+ fun create(client: OkHttpClient): NobilRealtimeApi {
+ val retrofit = Retrofit.Builder()
+ .baseUrl("https://api.ev-map.app/nobil/api/realtime/")
+ .addConverterFactory(
+ MoshiConverterFactory.create(
+ Moshi.Builder().add(InstantStringAdapter()).build()
+ )
+ )
+ .client(client)
+ .build()
+ return retrofit.create(NobilRealtimeApi::class.java)
+ }
+ }
+}
+
+@JsonClass(generateAdapter = true)
+data class NobilChargepointState(
+ val evseUid: String,
+ val status: String,
+ val timestamp: Instant
+)
+
+class NobilAvailabilityDetector(client: OkHttpClient, context: Context) :
+ BaseAvailabilityDetector(client) {
+ val api = NobilRealtimeApi.create(client)
+ val apiKey = context.getString(R.string.evmap_key)
+
+ override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
+ val nobilId = when (location.address?.country) {
+ "Norway" -> "NOR"
+ "Sweden" -> "SWE"
+ else -> throw AvailabilityDetectorException("nobil: unsupported country")
+ } + "_%05d".format(location.id)
+
+ val availability = api.getAvailability(nobilId, apiKey)
+ if (availability.isEmpty()) {
+ throw AvailabilityDetectorException("nobil: no real-time data available")
+ }
+ return ChargeLocationStatus(
+ location.chargepointsMerged.associateWith { cp ->
+ cp.evseUIds!!.map { evseUId ->
+ when (availability.find { it.evseUid == evseUId }?.status) {
+ "AVAILABLE" -> ChargepointStatus.AVAILABLE
+ "BLOCKED" -> ChargepointStatus.OCCUPIED
+ "CHARGING" -> ChargepointStatus.CHARGING
+ "INOPERATIVE" -> ChargepointStatus.FAULTED
+ "OUTOFORDER" -> ChargepointStatus.FAULTED
+ "PLANNED" -> ChargepointStatus.FAULTED
+ "REMOVED" -> ChargepointStatus.FAULTED
+ "RESERVED" -> ChargepointStatus.OCCUPIED
+ "UNKNOWN" -> ChargepointStatus.UNKNOWN
+ else -> ChargepointStatus.UNKNOWN
+ }
+ }
+ },
+ "Nobil",
+ location.chargepointsMerged.associateWith { cp ->
+ if (cp.evseIds != null) cp.evseIds.map { it ?: "??" } else listOf()
+ },
+ lastChange = location.chargepointsMerged.associateWith { cp ->
+ cp.evseUIds!!.map { evseUId ->
+ availability.find { it.evseUid == evseUId }?.timestamp
+ }
+ }
+ )
+ }
+
+ override fun isChargerSupported(charger: ChargeLocation): Boolean {
+ return when (charger.dataSource) {
+ "nobil" -> charger.chargepoints.any { it.evseUIds?.isNotEmpty() == true }
+ else -> false
+ }
+ }
+}
diff --git a/app/src/main/java/net/vonforst/evmap/api/nobil/NobilModel.kt b/app/src/main/java/net/vonforst/evmap/api/nobil/NobilModel.kt
index 7fd2b2517..ff9ba8d2d 100644
--- a/app/src/main/java/net/vonforst/evmap/api/nobil/NobilModel.kt
+++ b/app/src/main/java/net/vonforst/evmap/api/nobil/NobilModel.kt
@@ -279,9 +279,10 @@ data class NobilChargerStation(
val connectionVoltage = if (attribs["12"]?.attrVal is String) attribs["12"]?.attrVal.toString().toDoubleOrNull() else null
val connectionCurrent = if (attribs["31"]?.attrVal is String) attribs["31"]?.attrVal.toString().toDoubleOrNull() else null
+ val evseUId = if (attribs["27"]?.attrVal is String) listOf(attribs["27"]?.attrVal.toString()) else null
val evseId = if (attribs["28"]?.attrVal is String) listOf(attribs["28"]?.attrVal.toString()) else null
- return Chargepoint(connectionType, connectionPower, 1, connectionCurrent, connectionVoltage, evseId)
+ return Chargepoint(connectionType, connectionPower, 1, connectionCurrent, connectionVoltage, evseId, evseUId)
}
}
}
diff --git a/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt b/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt
index 322fc013f..99851ad93 100644
--- a/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt
+++ b/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt
@@ -140,10 +140,12 @@ data class ChargeLocation(
.filter { it.type == variant.type && it.power == variant.power }
val count = filtered.sumOf { it.count }
val mergedEvseIds = filtered.map { if (it.evseIds == null) List(it.count) {null} else it.evseIds }.flatten()
+ val mergedEvseUIds = filtered.map { if (it.evseUIds == null) List(it.count) {null} else it.evseUIds }.flatten()
Chargepoint(variant.type, variant.power, count,
filtered.map { it.current }.distinct().singleOrNull(),
filtered.map { it.voltage }.distinct().singleOrNull(),
- if (mergedEvseIds.all { it == null }) null else mergedEvseIds
+ if (mergedEvseIds.all { it == null }) null else mergedEvseIds,
+ if (mergedEvseUIds.all { it == null }) null else mergedEvseUIds
)
}
}
@@ -425,7 +427,9 @@ data class Chargepoint(
// (each of the three can be separately limited)
val voltage: Double? = null,
// Electric Vehicle Supply Equipment Ids for this Chargepoint's plugs/sockets
- val evseIds: List? = null
+ val evseIds: List? = null,
+ // Electric Vehicle Supply Equipment Unique Ids for this Chargepoint's plugs/sockets
+ val evseUIds: List? = null
) : Equatable, Parcelable {
fun hasKnownPower(): Boolean = power != null
fun hasKnownVoltageAndCurrent(): Boolean = voltage != null && current != null
diff --git a/app/src/main/java/net/vonforst/evmap/model/FavoritesModel.kt b/app/src/main/java/net/vonforst/evmap/model/FavoritesModel.kt
index cfd0f34f5..3a443e600 100644
--- a/app/src/main/java/net/vonforst/evmap/model/FavoritesModel.kt
+++ b/app/src/main/java/net/vonforst/evmap/model/FavoritesModel.kt
@@ -25,4 +25,4 @@ data class Favorite(
data class FavoriteWithDetail(
@Embedded val favorite: Favorite,
@Embedded val charger: ChargeLocation
-)
+)
\ No newline at end of file
diff --git a/app/src/main/java/net/vonforst/evmap/storage/Database.kt b/app/src/main/java/net/vonforst/evmap/storage/Database.kt
index 32b4c4c51..662684d1f 100644
--- a/app/src/main/java/net/vonforst/evmap/storage/Database.kt
+++ b/app/src/main/java/net/vonforst/evmap/storage/Database.kt
@@ -40,7 +40,7 @@ import net.vonforst.evmap.model.SliderFilterValue
OCMOperator::class,
OSMNetwork::class,
SavedRegion::class
- ], version = 27
+ ], version = 28
)
@TypeConverters(Converters::class, GeometryConverters::class)
abstract class AppDatabase : RoomDatabase() {
@@ -85,7 +85,7 @@ abstract class AppDatabase : RoomDatabase() {
MIGRATION_12, MIGRATION_13, MIGRATION_14, MIGRATION_15, MIGRATION_16,
MIGRATION_17, MIGRATION_18, MIGRATION_19, MIGRATION_20, MIGRATION_21,
MIGRATION_22, MIGRATION_23, MIGRATION_24, MIGRATION_25, MIGRATION_26,
- MIGRATION_27
+ MIGRATION_27, MIGRATION_28
)
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
@@ -547,6 +547,14 @@ abstract class AppDatabase : RoomDatabase() {
db.execSQL("ALTER TABLE `ChargeLocation` ADD `accessibility` TEXT")
}
}
+
+ private val MIGRATION_28 = object : Migration(27, 28) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ // Force nobil data refresh to fetch EVSE UId attributes needed for real-time data
+ db.execSQL("DELETE FROM SavedRegion WHERE `dataSource` = 'nobil'")
+ db.execSQL("DELETE FROM ChargeLocation WHERE `dataSource` = 'nobil'")
+ }
+ }
}
/**
diff --git a/app/src/main/java/net/vonforst/evmap/storage/FavoritesDao.kt b/app/src/main/java/net/vonforst/evmap/storage/FavoritesDao.kt
index 309d2f8fd..28eb8e888 100644
--- a/app/src/main/java/net/vonforst/evmap/storage/FavoritesDao.kt
+++ b/app/src/main/java/net/vonforst/evmap/storage/FavoritesDao.kt
@@ -13,10 +13,10 @@ interface FavoritesDao {
@Delete
suspend fun delete(vararg favorites: Favorite)
- @Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id")
+ @Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id WHERE chargelocation.id is not NULL")
fun getAllFavorites(): LiveData>
- @Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id")
+ @Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id WHERE chargelocation.id is not NULL")
suspend fun getAllFavoritesAsync(): List
@SkipQueryVerification
diff --git a/doc/api_keys.md b/doc/api_keys.md
index 4f9da8c49..a36f7bd69 100644
--- a/doc/api_keys.md
+++ b/doc/api_keys.md
@@ -38,6 +38,9 @@ be put into the app in the form of a resource file called `apikeys.xml` under
insert your nobil key here
+
+ insert your EVMap key here
+
```
@@ -236,6 +239,13 @@ key and documentation.
If you don't want to test this functionality, simply leave the API key blank.
+### EVMap
+
+EVMap provides APIs to fetch Nobil real-time data.
+
+Contact [EVMap](mailto:evmap@vonforst.net) to get an API key.
+
+
Crash reporting
---------------