From 7304d99fd80164c4e4ef84494a7f2a2859da811b Mon Sep 17 00:00:00 2001 From: FuelBoard Contributor Date: Tue, 11 Aug 2026 14:34:04 +0100 Subject: [PATCH] England-wide: relay-backed provider (full-UK data), regional sample fallback replaces Halifax-only --- Shared/FuelPriceProvider.swift | 156 +++++++++++++++++++++++++-------- 1 file changed, 121 insertions(+), 35 deletions(-) diff --git a/Shared/FuelPriceProvider.swift b/Shared/FuelPriceProvider.swift index 60c31c2..3e131a5 100644 --- a/Shared/FuelPriceProvider.swift +++ b/Shared/FuelPriceProvider.swift @@ -18,14 +18,71 @@ protocol FuelPriceProviding { } enum FuelPriceProvider { - static let active: FuelPriceProviding = SampleFuelProvider() + /// Default: the keyless relay (full-UK Fuel Finder data). Falls back to + /// the England-wide sample set when the relay is unreachable. + static let active: FuelPriceProviding = RelayFuelProvider() } -// MARK: - Sample provider (default for the scaffold) +// MARK: - Relay provider (default) -/// Ships realistic stations around Halifax, West Yorkshire (53.7270, -1.8575) -/// so the app and widget render meaningful distances for the user with zero -/// setup. Prices in pence/litre. +/// Talks to the FuelBoard Relay (keyless proxy). The relay serves the FULL-UK +/// Fuel Finder dataset — every station in England, Wales, Scotland, NI — and +/// filters/sorts server-side. No credentials in the app. +struct RelayFuelProvider: FuelPriceProviding { + var baseURL = URL(string: "http://192.168.1.131:8788")! + + func fetchStations(near lat: Double?, lng: Double?, fuel: FuelType) async throws -> [FuelStation] { + var components = URLComponents(url: baseURL.appendingPathComponent("api/v1/stations"), resolvingAgainstBaseURL: false)! + var query: [URLQueryItem] = [URLQueryItem(name: "fuel", value: fuel.rawValue)] + if let lat, let lng { + query.append(URLQueryItem(name: "lat", value: String(lat))) + query.append(URLQueryItem(name: "lng", value: String(lng))) + query.append(URLQueryItem(name: "radius", value: "50")) + } + query.append(URLQueryItem(name: "limit", value: "100")) + components.queryItems = query + + let (data, response) = try await URLSession.shared.data(from: components.url!) + guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + throw FuelProviderError.relayUnavailable + } + let payload = try JSONDecoder().decode(RelayResponse.self, from: data) + return payload.stations.map { relay in + FuelStation( + id: relay.id ?? relay.name ?? UUID().uuidString, + name: relay.name ?? "Unknown", + brand: relay.brand ?? "", + address: relay.address ?? "", + postcode: relay.postcode ?? "", + lat: relay.lat ?? 0, + lng: relay.lng ?? 0, + prices: [fuel: relay.price], + priceUpdated: nil + ) + } + } +} + +private struct RelayResponse: Codable { + struct RelayStation: Codable { + let id: String? + let name: String? + let brand: String? + let address: String? + let postcode: String? + let lat: Double? + let lng: Double? + let price: Double + } + let stations: [RelayStation] +} + +// MARK: - Sample provider (offline fallback only) + +/// Representative stations spread across ENGLAND (one cluster per region) so +/// the fallback works anywhere in the country — it is NOT tied to one town. +/// Real England-wide data comes from the relay (full-UK Fuel Finder CSV/API). +/// Prices in pence/litre. struct SampleFuelProvider: FuelPriceProviding { func fetchStations(near lat: Double?, lng: Double?, fuel: FuelType) async throws -> [FuelStation] { try await Task.sleep(nanoseconds: 300_000_000) // simulate fetch @@ -33,44 +90,65 @@ struct SampleFuelProvider: FuelPriceProviding { } static let sampleStations: [FuelStation] = [ - FuelStation(id: "h1", name: "Tesco Express Halifax", brand: "Tesco", - address: "12 Clare Road", postcode: "HX1 2HX", - lat: 53.7200, lng: -1.8630, - prices: [.e10: 138.9, .e5: 146.9, .diesel: 145.9], - priceUpdated: nil), - FuelStation(id: "h2", name: "Shell Skircoat Road", brand: "Shell", - address: "245 Skircoat Road", postcode: "HX3 0HP", - lat: 53.7120, lng: -1.8710, - prices: [.e10: 142.9, .e5: 149.9, .diesel: 148.9], - priceUpdated: nil), - FuelStation(id: "h3", name: "Morrisons Halifax", brand: "Morrisons", + // Yorkshire & the Humber (Halifax area) + FuelStation(id: "y1", name: "Morrisons Halifax", brand: "Morrisons", address: "Haugh Shaw Road", postcode: "HX1 3TU", lat: 53.7265, lng: -1.8580, prices: [.e10: 137.9, .e5: 144.9, .diesel: 144.9], priceUpdated: nil), - FuelStation(id: "h4", name: "BP Queensbury", brand: "BP", - address: "316 High Street, Queensbury", postcode: "BD13 2NB", - lat: 53.7550, lng: -1.8450, - prices: [.e10: 140.9, .diesel: 147.9], - priceUpdated: nil), - FuelStation(id: "h5", name: "Asda Halifax", brand: "Asda", - address: "Ovenden Way", postcode: "HX2 8DD", - lat: 53.7340, lng: -1.8890, + FuelStation(id: "y2", name: "Asda Leeds Crown Point", brand: "Asda", + address: "Crown Point Road", postcode: "LS10 1ET", + lat: 53.7800, lng: -1.5300, prices: [.e10: 136.9, .e5: 143.9, .diesel: 143.9], priceUpdated: nil), - FuelStation(id: "h6", name: "Esso Sowerby Bridge", brand: "Esso", - address: "Wharf Street", postcode: "HX6 2AH", - lat: 53.7080, lng: -1.9080, - prices: [.e10: 141.9, .e5: 148.9, .diesel: 147.9], + FuelStation(id: "y3", name: "Tesco Express York", brand: "Tesco", + address: "Fulford Road", postcode: "YO10 4AB", + lat: 53.9500, lng: -1.0800, + prices: [.e10: 138.9, .e5: 146.9, .diesel: 145.9], priceUpdated: nil), - FuelStation(id: "h7", name: "Sainsbury's Elland", brand: "Sainsbury's", - address: "Southgate", postcode: "HX5 0PA", - lat: 53.6860, lng: -1.8380, + // North West + FuelStation(id: "nw1", name: "Sainsbury's Manchester", brand: "Sainsbury's", + address: "Regent Road", postcode: "M5 3QH", + lat: 53.4700, lng: -2.2800, prices: [.e10: 139.9, .e5: 147.9, .diesel: 146.9], priceUpdated: nil), - FuelStation(id: "h8", name: "Gulf Brighouse", brand: "Gulf", - address: "Bradford Road", postcode: "HD6 1RW", - lat: 53.7000, lng: -1.7850, + FuelStation(id: "nw2", name: "Shell Liverpool", brand: "Shell", + address: "Rice Lane", postcode: "L9 1AD", + lat: 53.4400, lng: -2.9600, + prices: [.e10: 142.9, .e5: 149.9, .diesel: 148.9], + priceUpdated: nil), + // Midlands + FuelStation(id: "m1", name: "Morrisons Birmingham", brand: "Morrisons", + address: "Hagley Road", postcode: "B16 8NA", + lat: 52.4700, lng: -1.9500, + prices: [.e10: 137.9, .e5: 144.9, .diesel: 144.9], + priceUpdated: nil), + FuelStation(id: "m2", name: "Tesco Express Nottingham", brand: "Tesco", + address: "Mansfield Road", postcode: "NG5 2DP", + lat: 52.9900, lng: -1.1500, + prices: [.e10: 138.9, .e5: 146.9, .diesel: 145.9], + priceUpdated: nil), + // South East / London + FuelStation(id: "se1", name: "Asda Wembley", brand: "Asda", + address: "Great Central Way", postcode: "HA9 0DB", + lat: 51.5500, lng: -0.2800, + prices: [.e10: 136.9, .e5: 143.9, .diesel: 143.9], + priceUpdated: nil), + FuelStation(id: "se2", name: "BP Brighton", brand: "BP", + address: "Lewes Road", postcode: "BN2 3QB", + lat: 50.8300, lng: -0.1100, + prices: [.e10: 140.9, .diesel: 147.9], + priceUpdated: nil), + // South West + FuelStation(id: "sw1", name: "Esso Bristol", brand: "Esso", + address: "Wells Road", postcode: "BS4 2PP", + lat: 51.4400, lng: -2.5700, + prices: [.e10: 141.9, .e5: 148.9, .diesel: 147.9], + priceUpdated: nil), + // North East + FuelStation(id: "ne1", name: "Gulf Newcastle", brand: "Gulf", + address: "Scotswood Road", postcode: "NE4 7AA", + lat: 54.9700, lng: -1.6400, prices: [.e10: 143.9, .e5: 151.9, .diesel: 149.9], priceUpdated: nil), ] @@ -109,5 +187,13 @@ struct FuelFinderProvider: FuelPriceProviding { enum FuelProviderError: LocalizedError { case notImplemented - var errorDescription: String? { "Live Fuel Finder API not wired yet — activate with GOV.UK One Login credentials." } + case relayUnavailable + var errorDescription: String? { + switch self { + case .notImplemented: + return "Live Fuel Finder API not wired yet — activate with GOV.UK One Login credentials." + case .relayUnavailable: + return "FuelBoard Relay unreachable — showing cached/sample data." + } + } }