England-wide: relay-backed provider (full-UK data), regional sample fallback replaces Halifax-only

This commit is contained in:
FuelBoard Contributor
2026-08-11 14:34:04 +01:00
parent 2e2b189b69
commit 7304d99fd8
+121 -35
View File
@@ -18,14 +18,71 @@ protocol FuelPriceProviding {
} }
enum FuelPriceProvider { 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) /// Talks to the FuelBoard Relay (keyless proxy). The relay serves the FULL-UK
/// so the app and widget render meaningful distances for the user with zero /// Fuel Finder dataset every station in England, Wales, Scotland, NI and
/// setup. Prices in pence/litre. /// 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 { struct SampleFuelProvider: FuelPriceProviding {
func fetchStations(near lat: Double?, lng: Double?, fuel: FuelType) async throws -> [FuelStation] { func fetchStations(near lat: Double?, lng: Double?, fuel: FuelType) async throws -> [FuelStation] {
try await Task.sleep(nanoseconds: 300_000_000) // simulate fetch try await Task.sleep(nanoseconds: 300_000_000) // simulate fetch
@@ -33,44 +90,65 @@ struct SampleFuelProvider: FuelPriceProviding {
} }
static let sampleStations: [FuelStation] = [ static let sampleStations: [FuelStation] = [
FuelStation(id: "h1", name: "Tesco Express Halifax", brand: "Tesco", // Yorkshire & the Humber (Halifax area)
address: "12 Clare Road", postcode: "HX1 2HX", FuelStation(id: "y1", name: "Morrisons Halifax", brand: "Morrisons",
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",
address: "Haugh Shaw Road", postcode: "HX1 3TU", address: "Haugh Shaw Road", postcode: "HX1 3TU",
lat: 53.7265, lng: -1.8580, lat: 53.7265, lng: -1.8580,
prices: [.e10: 137.9, .e5: 144.9, .diesel: 144.9], prices: [.e10: 137.9, .e5: 144.9, .diesel: 144.9],
priceUpdated: nil), priceUpdated: nil),
FuelStation(id: "h4", name: "BP Queensbury", brand: "BP", FuelStation(id: "y2", name: "Asda Leeds Crown Point", brand: "Asda",
address: "316 High Street, Queensbury", postcode: "BD13 2NB", address: "Crown Point Road", postcode: "LS10 1ET",
lat: 53.7550, lng: -1.8450, lat: 53.7800, lng: -1.5300,
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,
prices: [.e10: 136.9, .e5: 143.9, .diesel: 143.9], prices: [.e10: 136.9, .e5: 143.9, .diesel: 143.9],
priceUpdated: nil), priceUpdated: nil),
FuelStation(id: "h6", name: "Esso Sowerby Bridge", brand: "Esso", FuelStation(id: "y3", name: "Tesco Express York", brand: "Tesco",
address: "Wharf Street", postcode: "HX6 2AH", address: "Fulford Road", postcode: "YO10 4AB",
lat: 53.7080, lng: -1.9080, lat: 53.9500, lng: -1.0800,
prices: [.e10: 141.9, .e5: 148.9, .diesel: 147.9], prices: [.e10: 138.9, .e5: 146.9, .diesel: 145.9],
priceUpdated: nil), priceUpdated: nil),
FuelStation(id: "h7", name: "Sainsbury's Elland", brand: "Sainsbury's", // North West
address: "Southgate", postcode: "HX5 0PA", FuelStation(id: "nw1", name: "Sainsbury's Manchester", brand: "Sainsbury's",
lat: 53.6860, lng: -1.8380, address: "Regent Road", postcode: "M5 3QH",
lat: 53.4700, lng: -2.2800,
prices: [.e10: 139.9, .e5: 147.9, .diesel: 146.9], prices: [.e10: 139.9, .e5: 147.9, .diesel: 146.9],
priceUpdated: nil), priceUpdated: nil),
FuelStation(id: "h8", name: "Gulf Brighouse", brand: "Gulf", FuelStation(id: "nw2", name: "Shell Liverpool", brand: "Shell",
address: "Bradford Road", postcode: "HD6 1RW", address: "Rice Lane", postcode: "L9 1AD",
lat: 53.7000, lng: -1.7850, 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], prices: [.e10: 143.9, .e5: 151.9, .diesel: 149.9],
priceUpdated: nil), priceUpdated: nil),
] ]
@@ -109,5 +187,13 @@ struct FuelFinderProvider: FuelPriceProviding {
enum FuelProviderError: LocalizedError { enum FuelProviderError: LocalizedError {
case notImplemented 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."
}
}
} }