From 883b9769846e6adf0aafc350f7f70bc38676e272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20H=C3=B6gberg?= Date: Thu, 22 Jan 2026 22:04:41 +0100 Subject: [PATCH 1/2] Nobil: Add charge network filter Only networks from ocpi connected chargers are used. The reason for this is to keep the list of networks "short" and easier to use. Many (~600) chargers that were manually added by the community have a network name that is unique to a specific charger. --- .../30.json | 956 ++++++++++++++++++ .../net/vonforst/evmap/api/nobil/NobilApi.kt | 28 +- .../vonforst/evmap/api/nobil/NobilModel.kt | 5 - .../evmap/storage/ChargeLocationsDao.kt | 7 +- .../net/vonforst/evmap/storage/Database.kt | 19 +- .../evmap/storage/NobilReferenceDataDao.kt | 39 +- 6 files changed, 1038 insertions(+), 16 deletions(-) create mode 100644 app/schemas/net.vonforst.evmap.storage.AppDatabase/30.json diff --git a/app/schemas/net.vonforst.evmap.storage.AppDatabase/30.json b/app/schemas/net.vonforst.evmap.storage.AppDatabase/30.json new file mode 100644 index 000000000..8fdcde0a2 --- /dev/null +++ b/app/schemas/net.vonforst.evmap.storage.AppDatabase/30.json @@ -0,0 +1,956 @@ +{ + "formatVersion": 1, + "database": { + "version": 30, + "identityHash": "7c51f9a4a365d75342a092c1c39e6a04", + "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": "NobilNetwork", + "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": "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, '7c51f9a4a365d75342a092c1c39e6a04')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/api/nobil/NobilApi.kt b/app/src/main/java/net/vonforst/evmap/api/nobil/NobilApi.kt index e6b40e4ab..0041081ea 100644 --- a/app/src/main/java/net/vonforst/evmap/api/nobil/NobilApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/nobil/NobilApi.kt @@ -221,7 +221,7 @@ class NobilApiWrapper( } override suspend fun getReferenceData(): Resource { - return Resource.success(NobilReferenceData(0)) + throw NotImplementedError() } override fun getFilters( @@ -247,7 +247,13 @@ class NobilApiWrapper( "By appointment" to sp.getString(R.string.accessibility_by_appointment), "Residents" to sp.getString(R.string.accessibility_residents) ) + val refData = referenceData as NobilReferenceData + val networkMap = refData.networks.associateWith { it } return listOf( + MultipleChoiceFilter( + sp.getString(R.string.filter_networks), "networks", + networkMap, manyChoices = true + ), BooleanFilter(sp.getString(R.string.filter_free_parking), "freeparking"), BooleanFilter(sp.getString(R.string.filter_open_247), "open_247"), SliderFilter( @@ -286,6 +292,16 @@ class NobilApiWrapper( var requiresChargepointQuery = false val result = StringBuilder() + val networks = filters.getMultipleChoiceValue("networks") + if (networks != null && !networks.all) { + val networksList = if (networks.values.size == 0) { + "" + } else { + networks.values.joinToString(",") { DatabaseUtils.sqlEscapeString(it) } + } + result.append(" AND network IN (${networksList})") + } + if (filters.getBooleanValue("freeparking") == true) { result.append(" AND freeparking IS 1") } @@ -331,6 +347,8 @@ class NobilApiWrapper( } } +data class NobilReferenceData(val networks: List) : ReferenceData() + class NobilFullDownloadResult(private val data: NobilDynamicResponseData, private val numTotalChargepoints: Int) : FullDownloadResult { private var downloadProgress = 0f @@ -339,13 +357,19 @@ class NobilFullDownloadResult(private val data: NobilDynamicResponseData, override val chargers: Sequence get() { if (data.rights == null) throw JsonDataException("Rights field is missing in received data") + + val networks = mutableSetOf() return sequence { data.chargerStations?.forEachIndexed { i, it -> downloadProgress = i.toFloat() / numTotalChargepoints val charger = it.convert(data.rights, null) + + if (charger?.chargepoints?.any { it.evseUIds?.isNotEmpty() == true } == true) { + charger.network?.let { networks.add(it) } + } charger?.let { yield(charger) } } - refData = NobilReferenceData(0) + refData = NobilReferenceData(networks.toList()) } } override val progress: Float 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 171a17b02..c5b7432a0 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 @@ -14,17 +14,12 @@ import net.vonforst.evmap.model.Coordinate import net.vonforst.evmap.model.Cost import net.vonforst.evmap.model.FilterValues import net.vonforst.evmap.model.OpeningHours -import net.vonforst.evmap.model.ReferenceData import net.vonforst.evmap.model.getBooleanValue import net.vonforst.evmap.model.getMultipleChoiceValue import net.vonforst.evmap.model.getSliderValue import java.time.Instant import java.time.LocalDateTime -data class NobilReferenceData( - val dummy: Int -) : ReferenceData() - @JsonClass(generateAdapter = true) data class NobilNumChargepointsRequest( val apikey: String, diff --git a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt index 935e9255e..52030b6ab 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt @@ -24,6 +24,7 @@ import net.vonforst.evmap.api.StringProvider import net.vonforst.evmap.api.goingelectric.GEReferenceData import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper import net.vonforst.evmap.api.nobil.NobilApiWrapper +import net.vonforst.evmap.api.nobil.NobilReferenceData import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper import net.vonforst.evmap.api.openstreetmap.OSMReferenceData import net.vonforst.evmap.api.openstreetmap.OpenStreetMapApiWrapper @@ -201,7 +202,7 @@ class ChargeLocationsRepository( } is NobilApiWrapper -> { - NobilReferenceDataRepository(scope, prefs).getReferenceData() + NobilReferenceDataRepository(db.nobilReferenceDataDao()).getReferenceData() } is OpenChargeMapApiWrapper -> { @@ -738,6 +739,10 @@ class ChargeLocationsRepository( ) when (api) { + is NobilApiWrapper -> { + val refData = result.referenceData + NobilReferenceDataRepository(db.nobilReferenceDataDao()).updateReferenceData(refData as NobilReferenceData) + } is OpenStreetMapApiWrapper -> { val refData = result.referenceData OSMReferenceDataRepository(db.osmReferenceDataDao()).updateReferenceData(refData as OSMReferenceData) 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 bf66c2f26..3c1e863b4 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/Database.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/Database.kt @@ -35,12 +35,13 @@ import net.vonforst.evmap.model.SliderFilterValue GEPlug::class, GENetwork::class, GEChargeCard::class, + NobilNetwork::class, OCMConnectionType::class, OCMCountry::class, OCMOperator::class, OSMNetwork::class, SavedRegion::class - ], version = 29 + ], version = 30 ) @TypeConverters(Converters::class, GeometryConverters::class) abstract class AppDatabase : RoomDatabase() { @@ -54,6 +55,9 @@ abstract class AppDatabase : RoomDatabase() { // GoingElectric API specific abstract fun geReferenceDataDao(): GEReferenceDataDao + // Nobil API specific + abstract fun nobilReferenceDataDao(): NobilReferenceDataDao + // OpenChargeMap API specific abstract fun ocmReferenceDataDao(): OCMReferenceDataDao @@ -85,7 +89,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_28, MIGRATION_29 + MIGRATION_27, MIGRATION_28, MIGRATION_29, MIGRATION_30 ) .addCallback(object : Callback() { override fun onCreate(db: SupportSQLiteDatabase) { @@ -563,6 +567,17 @@ abstract class AppDatabase : RoomDatabase() { db.execSQL("DELETE FROM ChargeLocation WHERE `dataSource` = 'nobil'") } } + + private val MIGRATION_30 = object : Migration(29, 30) { + override fun migrate(db: SupportSQLiteDatabase) { + // Nobil networks db table added + db.execSQL("CREATE TABLE IF NOT EXISTS `NobilNetwork` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))") + + // Force nobil data refresh to update networks list + 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/NobilReferenceDataDao.kt b/app/src/main/java/net/vonforst/evmap/storage/NobilReferenceDataDao.kt index 2d0fce488..2e095db64 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/NobilReferenceDataDao.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/NobilReferenceDataDao.kt @@ -10,17 +10,44 @@ import net.vonforst.evmap.viewmodel.Status import java.time.Duration import java.time.Instant +@Entity +data class NobilNetwork(@PrimaryKey val name: String) + @Dao abstract class NobilReferenceDataDao { + // NETWORKS + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract suspend fun insert(vararg networks: NobilNetwork) + + @Query("DELETE FROM nobilnetwork") + abstract fun deleteAllNetworks() + + @Transaction + open suspend fun updateNetworks(networks: List) { + deleteAllNetworks() + for (network in networks) { + insert(network) + } + } + + @Query("SELECT * FROM nobilnetwork") + abstract fun getAllNetworks(): LiveData> } -class NobilReferenceDataRepository( - private val scope: CoroutineScope, - private val prefs: PreferenceDataSource -) { + +class NobilReferenceDataRepository(private val dao: NobilReferenceDataDao) { fun getReferenceData(): LiveData { + val networks = dao.getAllNetworks() return MediatorLiveData().apply { - value = NobilReferenceData(0) + value = null + addSource(networks) { _ -> + val n = networks.value ?: return@addSource + value = NobilReferenceData(n.map { it.name }) + } } } -} \ No newline at end of file + + suspend fun updateReferenceData(refData: NobilReferenceData) { + dao.updateNetworks(refData.networks.map { NobilNetwork(it) }) + } +} From 460669b965a53f610e5f6877d59385e6087fc2b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20H=C3=B6gberg?= Date: Thu, 3 Sep 2026 20:09:31 +0200 Subject: [PATCH 2/2] Nobil: Merge networks with similar names when filtering on network This merges networks that use inconsisten spelling or slightly different names in Norway compared to Sweden. --- .../net/vonforst/evmap/api/nobil/NobilApi.kt | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/vonforst/evmap/api/nobil/NobilApi.kt b/app/src/main/java/net/vonforst/evmap/api/nobil/NobilApi.kt index 0041081ea..6bbbbe4e9 100644 --- a/app/src/main/java/net/vonforst/evmap/api/nobil/NobilApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/nobil/NobilApi.kt @@ -46,6 +46,21 @@ import java.io.IOException import java.time.Duration private const val maxResults = 2000 +private val networkAliases = listOf( + listOf("Connect GO", "Connect Go"), + listOf("E.ON", "E.ON Drive Public Sweden", "E.ON Energilösningar AB", "E.ON Norge AS"), + listOf("Eviny", "Eviny Sverige"), + listOf("Hedemora Energi", "Hedemor Energi"), + listOf("Mer", "MER Sweden AB", "Mer Norway"), + listOf("OKQ8", "Heavy Duty OKQ8 SE"), + listOf("Parkering Göteborg", "Göteborgs Stads Parkering"), + listOf("Pay n Charge", "Pay N Charge Nordic AB", "Pay n Charge Norge AS"), + listOf("RE&GO", "Re&Go", "RE&GO AB"), + listOf("Stella Energy", "STELLA ENERGY NORWAY AS"), + listOf("Svorka", "Svorka AS"), + listOf("Virta", "Virta Nordic AB", "Virta Global Sweden"), + listOf("Wattif", "Wattif Sweden"), + ) interface NobilApi { @GET("datadump.php") @@ -248,7 +263,14 @@ class NobilApiWrapper( "Residents" to sp.getString(R.string.accessibility_residents) ) val refData = referenceData as NobilReferenceData - val networkMap = refData.networks.associateWith { it } + val networkMap = refData.networks.map { n -> + networkAliases.forEach { aliasGroup -> + if (aliasGroup.contains(n)) { + return@map aliasGroup[0] + } + } + n + }.associateWith { it } return listOf( MultipleChoiceFilter( sp.getString(R.string.filter_networks), "networks", @@ -297,7 +319,15 @@ class NobilApiWrapper( val networksList = if (networks.values.size == 0) { "" } else { - networks.values.joinToString(",") { DatabaseUtils.sqlEscapeString(it) } + val networksWithAliases = networks.values.map { n -> + networkAliases.forEach { aliasGroup -> + if (aliasGroup.contains(n)) { + return@map aliasGroup + } + } + listOf(n) + }.flatten() + networksWithAliases.joinToString(",") { DatabaseUtils.sqlEscapeString(it) } } result.append(" AND network IN (${networksList})") }