From 04fd9a0065058977e7ad1c18e1158d4f11c2a3e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Tue, 26 May 2026 14:26:39 +0200 Subject: [PATCH 1/5] Add "in_zones" array when reporting zone-only location --- Sources/Shared/API/HAAPI.swift | 7 ++++-- Sources/Shared/API/Models/RealmZone.swift | 12 +++++++-- .../API/Models/WebhookUpdateLocation.swift | 5 +++- Sources/Shared/Environment/AppConstants.swift | 2 ++ Tests/Shared/RealmZone.test.swift | 17 +++++++++++++ .../Webhook/WebhookUpdateLocation.test.swift | 25 +++++++++++++++++++ 6 files changed, 63 insertions(+), 5 deletions(-) diff --git a/Sources/Shared/API/HAAPI.swift b/Sources/Shared/API/HAAPI.swift index 2b842d4ee9..fa28fde2fb 100644 --- a/Sources/Shared/API/HAAPI.swift +++ b/Sources/Shared/API/HAAPI.swift @@ -666,11 +666,14 @@ public class HomeAssistantAPI { update = .init(trigger: updateType, location: rawLocation, zone: zone) location = rawLocation case .zoneOnly: + let supportsInZones = server.info.version >= .inZonesOnLocationUpdate if updateType == .BeaconRegionEnter { - update = .init(trigger: updateType, usingNameOf: zone) + let zones = zone.flatMap { $0.TrackingEnabled && !$0.isPassive ? [$0] : nil } ?? [] + update = .init(trigger: updateType, usingNameOf: zones.first, inZones: supportsInZones ? zones : nil) } else if let rawLocation { // note this is a different zone than the event - e.g. the zone may be the one we are exiting - update = .init(trigger: updateType, usingNameOf: RLMZone.zone(of: rawLocation, in: server)) + let zones = RLMZone.zones(of: rawLocation, in: server, includingPassive: false) + update = .init(trigger: updateType, usingNameOf: zones.first, inZones: supportsInZones ? zones : nil) } else { update = .init(trigger: updateType) } diff --git a/Sources/Shared/API/Models/RealmZone.swift b/Sources/Shared/API/Models/RealmZone.swift index 7e4f5340cd..b07db9d835 100644 --- a/Sources/Shared/API/Models/RealmZone.swift +++ b/Sources/Shared/API/Models/RealmZone.swift @@ -247,16 +247,24 @@ public final class RLMZone: Object, UpdatableModel { "Zone - ID: \(identifier), state: " + (inRegion ? "inside" : "outside") } - public static func zone(of location: CLLocation, in server: Server) -> Self? { + public static func zones( + of location: CLLocation, + in server: Server, + includingPassive: Bool = true + ) -> [Self] { Current.realm() .objects(Self.self) .filter("%K == %@", #keyPath(serverIdentifier), server.identifier.rawValue) .filter("TrackingEnabled == true") + .filter { includingPassive || !$0.isPassive } .filter { $0.circularRegion.containsWithAccuracy(location) } .sorted { zoneA, zoneB in // match the smaller zone over the larger zoneA.Radius < zoneB.Radius } - .first + } + + public static func zone(of location: CLLocation, in server: Server) -> Self? { + zones(of: location, in: server).first } } diff --git a/Sources/Shared/API/Models/WebhookUpdateLocation.swift b/Sources/Shared/API/Models/WebhookUpdateLocation.swift index 5a73032f26..5d88b6152d 100644 --- a/Sources/Shared/API/Models/WebhookUpdateLocation.swift +++ b/Sources/Shared/API/Models/WebhookUpdateLocation.swift @@ -13,6 +13,7 @@ public struct WebhookUpdateLocation: ImmutableMappable { public var battery: Int? public var location: CLLocationCoordinate2D? public var locationName: String? + public var inZones: [String]? public var speed: CLLocationSpeed? public var altitude: CLLocationDistance? @@ -29,9 +30,10 @@ public struct WebhookUpdateLocation: ImmutableMappable { } } - public init(trigger: LocationUpdateTrigger, usingNameOf zone: RLMZone?) { + public init(trigger: LocationUpdateTrigger, usingNameOf zone: RLMZone?, inZones: [RLMZone]? = nil) { self.init(trigger: trigger) self.locationName = zone?.deviceTrackerName ?? LocationNames.NotHome.rawValue + self.inZones = inZones?.map(\.entityId) } public init(trigger: LocationUpdateTrigger, location: CLLocation?, zone: RLMZone?) { @@ -111,6 +113,7 @@ public struct WebhookUpdateLocation: ImmutableMappable { location >>> (map["gps"], CLLocationCoordinate2DTransform()) horizontalAccuracy >>> map["gps_accuracy"] locationName >>> map["location_name"] + inZones >>> map["in_zones"] speed >>> map["speed"] altitude >>> map["altitude"] course >>> map["course"] diff --git a/Sources/Shared/Environment/AppConstants.swift b/Sources/Shared/Environment/AppConstants.swift index 85d47b3446..900d2c11ba 100644 --- a/Sources/Shared/Environment/AppConstants.swift +++ b/Sources/Shared/Environment/AppConstants.swift @@ -386,6 +386,8 @@ public extension Version { static let canNavigateMoreInfoDialogThroughFrontend: Version = .init(major: 2026, minor: 1, prerelease: "any0") /// Frontend introduces the quickbar with Ctrl+K keyboard shortcut in 2026.2 static let quickSearchKeyboardShortcut: Version = .init(major: 2026, minor: 2, prerelease: "any0") + /// Core accepts `in_zones` in update_location payloads from 2026.6.0. + static let inZonesOnLocationUpdate: Version = .init(major: 2026, minor: 6, patch: 0) var coreRequiredString: String { L10n.requiresVersion(String(format: "core-%d.%d", major, minor ?? -1)) diff --git a/Tests/Shared/RealmZone.test.swift b/Tests/Shared/RealmZone.test.swift index ad0a489539..091093aa06 100644 --- a/Tests/Shared/RealmZone.test.swift +++ b/Tests/Shared/RealmZone.test.swift @@ -169,6 +169,14 @@ class RealmZoneTests: XCTestCase { in: server1 ) XCTAssertEqual(inside1?.entityId, "zone1_b", "should prefer smaller") + XCTAssertEqual( + RLMZone.zones( + of: CLLocation(latitude: 37.77427675230296, longitude: -122.39145063179514), + in: server1 + ).map(\.entityId), + ["zone1_b", "zone1_a"], + "should return all matching zones, sorted by radius" + ) let inside2 = RLMZone.zone( of: CLLocation(latitude: 37.76392336744542, longitude: -122.41274993932525), @@ -187,6 +195,15 @@ class RealmZoneTests: XCTestCase { in: server1 ) XCTAssertEqual(insidePassive?.entityId, "zone_passive", "passive zone with TrackingEnabled should be returned") + XCTAssertEqual( + RLMZone.zones( + of: CLLocation(latitude: 37.80535, longitude: -122.43194), + in: server1, + includingPassive: false + ).map(\.entityId), + [], + "passive zones should be excluded when requested" + ) let insideDisabled = RLMZone.zone( of: CLLocation(latitude: 37.80290, longitude: -122.45290), diff --git a/Tests/Shared/Webhook/WebhookUpdateLocation.test.swift b/Tests/Shared/Webhook/WebhookUpdateLocation.test.swift index 48035f347e..a11741764b 100644 --- a/Tests/Shared/Webhook/WebhookUpdateLocation.test.swift +++ b/Tests/Shared/Webhook/WebhookUpdateLocation.test.swift @@ -49,6 +49,31 @@ class WebhookUpdateLocationTests: XCTestCase { XCTAssertNil(json["vertical_accuracy"]) } + func testNameOfZoneWithInZones() { + Current.device.batteries = { [DeviceBattery(level: 44, state: .charging, attributes: [:])] } + + let zone = with(RLMZone()) { + $0.entityId = "zone.given_name" + $0.serverIdentifier = "server1" + } + let otherZone = with(RLMZone()) { + $0.entityId = "zone.other_name" + $0.serverIdentifier = "server1" + } + + let model = WebhookUpdateLocation(trigger: .GPSRegionEnter, usingNameOf: zone, inZones: [zone, otherZone]) + let json = model.toJSON() + XCTAssertEqual(json["battery"] as? Int, 44) + XCTAssertEqual(json["location_name"] as? String, "given_name") + XCTAssertEqual(json["in_zones"] as? [String], ["zone.given_name", "zone.other_name"]) + XCTAssertNil(json["gps"]) + XCTAssertNil(json["gps_accuracy"]) + XCTAssertNil(json["speed"]) + XCTAssertNil(json["altitude"]) + XCTAssertNil(json["course"]) + XCTAssertNil(json["vertical_accuracy"]) + } + func testNameOfZoneWithNoZone() { Current.device.batteries = { [DeviceBattery(level: 44, state: .charging, attributes: [:])] } From fe612b6f8e0d57371d2e193a24754390b57cfe8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Wed, 27 May 2026 11:07:46 +0200 Subject: [PATCH 2/5] Remove passive zones filtering --- Sources/Shared/API/HAAPI.swift | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Sources/Shared/API/HAAPI.swift b/Sources/Shared/API/HAAPI.swift index fa28fde2fb..634a93ba9a 100644 --- a/Sources/Shared/API/HAAPI.swift +++ b/Sources/Shared/API/HAAPI.swift @@ -668,12 +668,22 @@ public class HomeAssistantAPI { case .zoneOnly: let supportsInZones = server.info.version >= .inZonesOnLocationUpdate if updateType == .BeaconRegionEnter { - let zones = zone.flatMap { $0.TrackingEnabled && !$0.isPassive ? [$0] : nil } ?? [] - update = .init(trigger: updateType, usingNameOf: zones.first, inZones: supportsInZones ? zones : nil) + let zones = zone.flatMap { $0.TrackingEnabled ? [$0] : nil } ?? [] + let locationNameZone = supportsInZones ? zones.first { !$0.isPassive } : zone + update = .init( + trigger: updateType, + usingNameOf: locationNameZone, + inZones: supportsInZones ? zones : nil + ) } else if let rawLocation { // note this is a different zone than the event - e.g. the zone may be the one we are exiting - let zones = RLMZone.zones(of: rawLocation, in: server, includingPassive: false) - update = .init(trigger: updateType, usingNameOf: zones.first, inZones: supportsInZones ? zones : nil) + let zones = RLMZone.zones(of: rawLocation, in: server) + let locationNameZone = supportsInZones ? zones.first { !$0.isPassive } : zones.first + update = .init( + trigger: updateType, + usingNameOf: locationNameZone, + inZones: supportsInZones ? zones : nil + ) } else { update = .init(trigger: updateType) } From 9ad27ca95d43419f55bc41bad704249a1872ac5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o=20Gon=C3=A7alves?= <5808343+bgoncal@users.noreply.github.com> Date: Wed, 27 May 2026 15:37:56 +0200 Subject: [PATCH 3/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Sources/Shared/Environment/AppConstants.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Shared/Environment/AppConstants.swift b/Sources/Shared/Environment/AppConstants.swift index 900d2c11ba..5c02fe8238 100644 --- a/Sources/Shared/Environment/AppConstants.swift +++ b/Sources/Shared/Environment/AppConstants.swift @@ -387,7 +387,7 @@ public extension Version { /// Frontend introduces the quickbar with Ctrl+K keyboard shortcut in 2026.2 static let quickSearchKeyboardShortcut: Version = .init(major: 2026, minor: 2, prerelease: "any0") /// Core accepts `in_zones` in update_location payloads from 2026.6.0. - static let inZonesOnLocationUpdate: Version = .init(major: 2026, minor: 6, patch: 0) + static let inZonesOnLocationUpdate: Version = .init(major: 2026, minor: 6, patch: 0, prerelease: "any0") var coreRequiredString: String { L10n.requiresVersion(String(format: "core-%d.%d", major, minor ?? -1)) From c25a1bdccf4df80cfa64c3e3ffa041dea9a0e419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o=20Gon=C3=A7alves?= <5808343+bgoncal@users.noreply.github.com> Date: Wed, 27 May 2026 15:38:30 +0200 Subject: [PATCH 4/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Sources/Shared/API/Models/RealmZone.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Sources/Shared/API/Models/RealmZone.swift b/Sources/Shared/API/Models/RealmZone.swift index b07db9d835..649eaf7d7b 100644 --- a/Sources/Shared/API/Models/RealmZone.swift +++ b/Sources/Shared/API/Models/RealmZone.swift @@ -252,11 +252,16 @@ public final class RLMZone: Object, UpdatableModel { in server: Server, includingPassive: Bool = true ) -> [Self] { - Current.realm() + var results = Current.realm() .objects(Self.self) .filter("%K == %@", #keyPath(serverIdentifier), server.identifier.rawValue) .filter("TrackingEnabled == true") - .filter { includingPassive || !$0.isPassive } + + if !includingPassive { + results = results.filter("isPassive == false") + } + + return results .filter { $0.circularRegion.containsWithAccuracy(location) } .sorted { zoneA, zoneB in // match the smaller zone over the larger From 68009059e2c65f0cdf08a9fe1068c89cdc04aa84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Thu, 28 May 2026 14:36:42 +0200 Subject: [PATCH 5/5] PR improvements --- Sources/Shared/API/HAAPI.swift | 54 +++++++++++++------ Sources/Shared/API/Models/RealmZone.swift | 6 +-- .../API/Models/WebhookUpdateLocation.swift | 3 +- .../Webhook/WebhookUpdateLocation.test.swift | 1 + 4 files changed, 45 insertions(+), 19 deletions(-) diff --git a/Sources/Shared/API/HAAPI.swift b/Sources/Shared/API/HAAPI.swift index aea7d1f646..cf54f9d735 100644 --- a/Sources/Shared/API/HAAPI.swift +++ b/Sources/Shared/API/HAAPI.swift @@ -646,6 +646,7 @@ public class HomeAssistantAPI { ) -> Promise { let update: WebhookUpdateLocation let location: CLLocation? + let supportsInZones = server.info.version >= .inZonesOnLocationUpdate let localMetadata = WebhookResponseLocation.localMetdata( trigger: updateType, zone: zone @@ -656,23 +657,17 @@ public class HomeAssistantAPI { update = .init(trigger: updateType, location: rawLocation, zone: zone) location = rawLocation case .zoneOnly: - let supportsInZones = server.info.version >= .inZonesOnLocationUpdate - if updateType == .BeaconRegionEnter { - let zones = zone.flatMap { $0.TrackingEnabled ? [$0] : nil } ?? [] - let locationNameZone = supportsInZones ? zones.first { !$0.isPassive } : zone + let inZones = zones(for: updateType, location: rawLocation, fallbackZone: zone) + if updateType == .BeaconRegionEnter || rawLocation != nil { update = .init( trigger: updateType, - usingNameOf: locationNameZone, - inZones: supportsInZones ? zones : nil - ) - } else if let rawLocation { - // note this is a different zone than the event - e.g. the zone may be the one we are exiting - let zones = RLMZone.zones(of: rawLocation, in: server) - let locationNameZone = supportsInZones ? zones.first { !$0.isPassive } : zones.first - update = .init( - trigger: updateType, - usingNameOf: locationNameZone, - inZones: supportsInZones ? zones : nil + usingNameOf: locationNameZone( + for: updateType, + from: inZones, + fallbackZone: zone, + supportsInZones: supportsInZones + ), + inZones: supportsInZones ? inZones : nil ) } else { update = .init(trigger: updateType) @@ -717,6 +712,35 @@ public class HomeAssistantAPI { }.asVoid() } + private func zones( + for updateType: LocationUpdateTrigger, + location rawLocation: CLLocation?, + fallbackZone zone: RLMZone? + ) -> [RLMZone] { + if updateType == .BeaconRegionEnter { + return zone.flatMap { $0.TrackingEnabled ? [$0] : nil } ?? [] + } else if let rawLocation { + return RLMZone.zones(of: rawLocation, in: server) + } else { + return [] + } + } + + private func locationNameZone( + for updateType: LocationUpdateTrigger, + from zones: [RLMZone], + fallbackZone zone: RLMZone?, + supportsInZones: Bool + ) -> RLMZone? { + if supportsInZones { + return zones.first { !$0.isPassive } + } else if updateType == .BeaconRegionEnter { + return zone + } else { + return zones.first + } + } + public var sharedEventDeviceInfo: [String: String] { [ "sourceDevicePermanentID": AppConstants.PermanentID, diff --git a/Sources/Shared/API/Models/RealmZone.swift b/Sources/Shared/API/Models/RealmZone.swift index 649eaf7d7b..cd0095df80 100644 --- a/Sources/Shared/API/Models/RealmZone.swift +++ b/Sources/Shared/API/Models/RealmZone.swift @@ -251,9 +251,9 @@ public final class RLMZone: Object, UpdatableModel { of location: CLLocation, in server: Server, includingPassive: Bool = true - ) -> [Self] { + ) -> [RLMZone] { var results = Current.realm() - .objects(Self.self) + .objects(RLMZone.self) .filter("%K == %@", #keyPath(serverIdentifier), server.identifier.rawValue) .filter("TrackingEnabled == true") @@ -269,7 +269,7 @@ public final class RLMZone: Object, UpdatableModel { } } - public static func zone(of location: CLLocation, in server: Server) -> Self? { + public static func zone(of location: CLLocation, in server: Server) -> RLMZone? { zones(of: location, in: server).first } } diff --git a/Sources/Shared/API/Models/WebhookUpdateLocation.swift b/Sources/Shared/API/Models/WebhookUpdateLocation.swift index 5d88b6152d..f74270dfa5 100644 --- a/Sources/Shared/API/Models/WebhookUpdateLocation.swift +++ b/Sources/Shared/API/Models/WebhookUpdateLocation.swift @@ -36,8 +36,9 @@ public struct WebhookUpdateLocation: ImmutableMappable { self.inZones = inZones?.map(\.entityId) } - public init(trigger: LocationUpdateTrigger, location: CLLocation?, zone: RLMZone?) { + public init(trigger: LocationUpdateTrigger, location: CLLocation?, zone: RLMZone?, inZones: [RLMZone]? = nil) { self.init(trigger: trigger) + self.inZones = inZones?.map(\.entityId) let useLocation: Bool diff --git a/Tests/Shared/Webhook/WebhookUpdateLocation.test.swift b/Tests/Shared/Webhook/WebhookUpdateLocation.test.swift index a11741764b..629ed827fd 100644 --- a/Tests/Shared/Webhook/WebhookUpdateLocation.test.swift +++ b/Tests/Shared/Webhook/WebhookUpdateLocation.test.swift @@ -326,6 +326,7 @@ class WebhookUpdateLocationTests: XCTestCase { XCTAssertEqual(json["gps"] as? [Double], [1.23, 4.56]) XCTAssertEqual(json["gps_accuracy"] as? Double, 104) XCTAssertNil(json["location_name"]) + XCTAssertNil(json["in_zones"]) XCTAssertEqual(json["speed"] as? Double, 108) XCTAssertEqual(json["altitude"] as? Double, 103) XCTAssertEqual(json["course"] as? Double, 106)