Skip to content

Commit

Permalink
Add iOS 18 control switch and abstract controls logic (#3039)
Browse files Browse the repository at this point in the history
  • Loading branch information
bgoncal authored Sep 23, 2024
1 parent 428f2e8 commit 4acaf55
Show file tree
Hide file tree
Showing 19 changed files with 459 additions and 168 deletions.
36 changes: 33 additions & 3 deletions HomeAssistant.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,7 @@
425573E92B58396600145217 /* HAEntity+CarPlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3BC66629BA003B00B19FBE /* HAEntity+CarPlay.swift */; };
425573EB2B588FFB00145217 /* CarPlayListItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425573EA2B588FFB00145217 /* CarPlayListItemProvider.swift */; };
425573ED2B58904000145217 /* CarPlayEntityListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425573EC2B58904000145217 /* CarPlayEntityListItem.swift */; };
425FBA1E2C9C75A300CB5DBB /* DataWidgetsUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42A47D4A2C9AEF10003C597D /* DataWidgetsUpdater.swift */; };
425FF0562C8216B3000AA641 /* AssistAppIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425FF0552C8216B3000AA641 /* AssistAppIntent.swift */; };
426266412C11A63A0081A818 /* SharedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 420B10032B1CF6D800D383D8 /* SharedAssets.xcassets */; };
426266422C11A6700081A818 /* SharedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 420B10032B1CF6D800D383D8 /* SharedAssets.xcassets */; };
Expand All @@ -602,6 +603,10 @@
426490772C0F2403002155CC /* WatchAudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426490762C0F2403002155CC /* WatchAudioRecorder.swift */; };
4264907A2C0F3D97002155CC /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4291068B2BA9D79500D452F9 /* AudioPlayer.swift */; };
426740A92B17391000C1DD73 /* Data+Hexadecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426740A72B17390A00C1DD73 /* Data+Hexadecimal.swift */; };
426CBB6A2C9C543F003CA3AC /* ControlSwitchValueProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426CBB692C9C543F003CA3AC /* ControlSwitchValueProvider.swift */; };
426CBB6C2C9C550D003CA3AC /* IntentSwitchEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426CBB6B2C9C550D003CA3AC /* IntentSwitchEntity.swift */; };
426D9C742C9C60B000F278AF /* ControlEntityProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426D9C722C9C582F00F278AF /* ControlEntityProvider.swift */; };
426D9C752C9C60B000F278AF /* ControlEntityProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426D9C722C9C582F00F278AF /* ControlEntityProvider.swift */; };
4273C4872C8857B00065A5B4 /* ControlOpenPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4273C4862C8857B00065A5B4 /* ControlOpenPage.swift */; };
4273C4882C8857B00065A5B4 /* ControlOpenPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4273C4862C8857B00065A5B4 /* ControlOpenPage.swift */; };
4273C48A2C8858470065A5B4 /* ControlOpenPageValueProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4273C4892C8858470065A5B4 /* ControlOpenPageValueProvider.swift */; };
Expand Down Expand Up @@ -666,7 +671,6 @@
42A47A8A2C452DB500C9B43D /* MockWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42A47A892C452DB500C9B43D /* MockWebViewController.swift */; };
42A47A8C2C4547B800C9B43D /* WebViewExternalMessageHandler+Build.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42A47A8B2C4547B800C9B43D /* WebViewExternalMessageHandler+Build.swift */; };
42A47A902C4548E100C9B43D /* ImprovDiscoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42A47A8F2C4548E100C9B43D /* ImprovDiscoverView.swift */; };
42A47D4B2C9AEF10003C597D /* DataWidgetsUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42A47D4A2C9AEF10003C597D /* DataWidgetsUpdater.swift */; };
42A818E02BBEA8150083D045 /* AssistViewModel.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42A818DF2BBEA8150083D045 /* AssistViewModel.test.swift */; };
42A818E32BBEA9780083D045 /* MockAudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42A818E22BBEA9780083D045 /* MockAudioRecorder.swift */; };
42A818E52BBEAA3A0083D045 /* MockAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42A818E42BBEAA3A0083D045 /* MockAudioPlayer.swift */; };
Expand Down Expand Up @@ -723,6 +727,8 @@
42EB03062C6E42F900A184A6 /* WatchHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EB03052C6E42F900A184A6 /* WatchHomeView.swift */; };
42EB03082C6E430300A184A6 /* WatchHomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EB03072C6E430300A184A6 /* WatchHomeViewModel.swift */; };
42EB030A2C6E4D0E00A184A6 /* WatchMagicViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EB03092C6E4D0E00A184A6 /* WatchMagicViewRow.swift */; };
42F158462CA15C99009C7201 /* ControlSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F158452CA15C99009C7201 /* ControlSwitch.swift */; };
42F158482CA15FA7009C7201 /* SwitchIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F158472CA15FA7009C7201 /* SwitchIntent.swift */; };
42F1DA5B2B4BF7DF002729BC /* WindowSizeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F1DA5A2B4BF7DF002729BC /* WindowSizeObserver.swift */; };
42F1DA5D2B4BF85F002729BC /* WindowScenesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F1DA5C2B4BF85F002729BC /* WindowScenesManager.swift */; };
42F1DA5F2B4D4B32002729BC /* CarPlayServerListTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F1DA5E2B4D4B32002729BC /* CarPlayServerListTemplate.swift */; };
Expand Down Expand Up @@ -1843,6 +1849,9 @@
426490742C0F20FF002155CC /* WatchAssistView+Build.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WatchAssistView+Build.swift"; sourceTree = "<group>"; };
426490762C0F2403002155CC /* WatchAudioRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchAudioRecorder.swift; sourceTree = "<group>"; };
426740A72B17390A00C1DD73 /* Data+Hexadecimal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Hexadecimal.swift"; sourceTree = "<group>"; };
426CBB692C9C543F003CA3AC /* ControlSwitchValueProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlSwitchValueProvider.swift; sourceTree = "<group>"; };
426CBB6B2C9C550D003CA3AC /* IntentSwitchEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentSwitchEntity.swift; sourceTree = "<group>"; };
426D9C722C9C582F00F278AF /* ControlEntityProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlEntityProvider.swift; sourceTree = "<group>"; };
4273C4862C8857B00065A5B4 /* ControlOpenPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlOpenPage.swift; sourceTree = "<group>"; };
4273C4892C8858470065A5B4 /* ControlOpenPageValueProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlOpenPageValueProvider.swift; sourceTree = "<group>"; };
4273C48C2C8859530065A5B4 /* PageAppEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageAppEntity.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1974,6 +1983,8 @@
42EB03052C6E42F900A184A6 /* WatchHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchHomeView.swift; sourceTree = "<group>"; };
42EB03072C6E430300A184A6 /* WatchHomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchHomeViewModel.swift; sourceTree = "<group>"; };
42EB03092C6E4D0E00A184A6 /* WatchMagicViewRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchMagicViewRow.swift; sourceTree = "<group>"; };
42F158452CA15C99009C7201 /* ControlSwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlSwitch.swift; sourceTree = "<group>"; };
42F158472CA15FA7009C7201 /* SwitchIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchIntent.swift; sourceTree = "<group>"; };
42F1DA572B46FDD8002729BC /* HATypedRequest+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HATypedRequest+App.swift"; sourceTree = "<group>"; };
42F1DA5A2B4BF7DF002729BC /* WindowSizeObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowSizeObserver.swift; sourceTree = "<group>"; };
42F1DA5C2B4BF85F002729BC /* WindowScenesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowScenesManager.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2822,6 +2833,7 @@
1171506E24DFCDE60065E874 /* Widgets */ = {
isa = PBXGroup;
children = (
426CBB682C9C5426003CA3AC /* Switch */,
420461672C8F29290062E89F /* Light */,
42F958962BB4680100497981 /* Assist */,
115560DF27010D6700A8F818 /* Common */,
Expand Down Expand Up @@ -3677,6 +3689,17 @@
path = Extensions;
sourceTree = "<group>";
};
426CBB682C9C5426003CA3AC /* Switch */ = {
isa = PBXGroup;
children = (
426CBB692C9C543F003CA3AC /* ControlSwitchValueProvider.swift */,
426CBB6B2C9C550D003CA3AC /* IntentSwitchEntity.swift */,
42F158452CA15C99009C7201 /* ControlSwitch.swift */,
42F158472CA15FA7009C7201 /* SwitchIntent.swift */,
);
path = Switch;
sourceTree = "<group>";
};
4273C4852C88579E0065A5B4 /* Control */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -3989,7 +4012,6 @@
isa = PBXGroup;
children = (
42FCCFDC2B9B1AB00057783F /* Camera */,
42A47D4A2C9AEF10003C597D /* DataWidgetsUpdater.swift */,
);
path = BarcodeScanner;
sourceTree = "<group>";
Expand Down Expand Up @@ -4646,6 +4668,8 @@
424D2D0F2C89DACE00C610F1 /* HAAppEntity.swift */,
42F1DA572B46FDD8002729BC /* HATypedRequest+App.swift */,
42F1DA6F2B4EE2E8002729BC /* HAAreaResponse.swift */,
426D9C722C9C582F00F278AF /* ControlEntityProvider.swift */,
42A47D4A2C9AEF10003C597D /* DataWidgetsUpdater.swift */,
4251AABD2C6CE230004CCC9D /* MagicItem */,
4264906A2C0F1B40002155CC /* Assist */,
426740A42B17348700C1DD73 /* Assets */,
Expand Down Expand Up @@ -6360,8 +6384,11 @@
42A935A22C7FEBA100FCF504 /* ControlScript.swift in Sources */,
427647222C8F36DB0027B21F /* LightIntent.swift in Sources */,
4296C3762B91F0F50051B63C /* WidgetActionsAppIntentTimelineProvider.swift in Sources */,
42F158482CA15FA7009C7201 /* SwitchIntent.swift in Sources */,
426CBB6C2C9C550D003CA3AC /* IntentSwitchEntity.swift in Sources */,
110E694424E77125004AA96D /* WidgetActionsProvider.swift in Sources */,
42BA1BC82C8864C200A2FC36 /* OpenPageAppIntent.swift in Sources */,
426CBB6A2C9C543F003CA3AC /* ControlSwitchValueProvider.swift in Sources */,
420E2AE52C4746CD004921D8 /* WidgetBasicSizeStyle.swift in Sources */,
424A7F482B188BF3008C8DF3 /* WidgetContentMargin.swift in Sources */,
115560E127010D8400A8F818 /* WidgetBasicContainerView.swift in Sources */,
Expand All @@ -6379,6 +6406,7 @@
4080D5C52C319B0A00099C88 /* WidgetDetailsAppIntentTimelineProvider.swift in Sources */,
4080D5C62C319B0A00099C88 /* WidgetDetailsAppIntent.swift in Sources */,
4296C3772B91F26A0051B63C /* IntentActionAppEntity.swift in Sources */,
42F158462CA15C99009C7201 /* ControlSwitch.swift in Sources */,
4289DDAF2C85D5C4003591C2 /* ControlScene.swift in Sources */,
4273C4872C8857B00065A5B4 /* ControlOpenPage.swift in Sources */,
42A2AB802C80751E00C5608D /* ControlAssist.swift in Sources */,
Expand Down Expand Up @@ -6623,7 +6651,6 @@
113FB1132515A065000AC680 /* ScaleFactorMutator.swift in Sources */,
4296C37A2B9205450051B63C /* WidgetActionsAppIntent.swift in Sources */,
1185DFB3271FF53800ED7D9A /* OnboardingAuthStepSensors.swift in Sources */,
42A47D4B2C9AEF10003C597D /* DataWidgetsUpdater.swift in Sources */,
11108D632634C8FE009DAB0F /* LearnMoreButtonRow.swift in Sources */,
425573D12B5576E600145217 /* CarPlayDomainsListTemplate+Build.swift in Sources */,
42266B112B740E4C00E94A71 /* BarcodeScannerView.swift in Sources */,
Expand Down Expand Up @@ -6788,6 +6815,7 @@
1110836924AFEFA60027A67A /* Promise+WebhookJson.swift in Sources */,
1164D9DF25FB1B9800515E8A /* UIBarButtonItem+Additions.swift in Sources */,
11B38EF6275C54A300205C7B /* PickAServerError.swift in Sources */,
426D9C752C9C60B000F278AF /* ControlEntityProvider.swift in Sources */,
B67CE8AF22200F220034C1D0 /* ObjectMapperTransformers.swift in Sources */,
11AF4D13249C7E08006C74C0 /* ActivitySensor.swift in Sources */,
11E5CF8224BBCE1B009AC30F /* ProcessInfo+BackgroundTask.swift in Sources */,
Expand Down Expand Up @@ -7094,6 +7122,7 @@
11B38EE5275C54A200205C7B /* SendLocationIntentHandler.swift in Sources */,
1120C57F274638330046C38B /* PerServerContainer.swift in Sources */,
42FCCFD62B9B195D0057783F /* Image+SharedAssets.swift in Sources */,
426D9C742C9C60B000F278AF /* ControlEntityProvider.swift in Sources */,
42F5CAE82B10CDC900409816 /* HAButton.swift in Sources */,
B672334D225DE1490031D629 /* SubscribeEvents.swift in Sources */,
424D2D102C89DACE00C610F1 /* HAAppEntity.swift in Sources */,
Expand Down Expand Up @@ -7192,6 +7221,7 @@
11F2F1EC2586ED6100F61F7C /* NotificationAttachmentManager.swift in Sources */,
11F855D824DF6C7A0018013E /* MaterialDesignIcons.swift in Sources */,
42FCD0072B9B1DA10057783F /* CollapsibleView.swift in Sources */,
425FBA1E2C9C75A300CB5DBB /* DataWidgetsUpdater.swift in Sources */,
11521BBC25400284009C5C72 /* CrashReporter.swift in Sources */,
11EE9B5424C62EB300404AF8 /* RealmScene.swift in Sources */,
11F2F26E25871D8200F61F7C /* NotificationAttachmentParserURL.swift in Sources */,
Expand Down
1 change: 1 addition & 0 deletions Sources/App/AppEntitiesObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ enum AppEntitiesObserver {
Domain.scene,
Domain.script,
Domain.light,
Domain.switch,
].map(\.rawValue)

func start() {
Expand Down
2 changes: 2 additions & 0 deletions Sources/App/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,9 @@ Home Assistant is free and open source home automation software with a focus on
"widgets.controls.assist.pipeline.placeholder" = "Choose a pipeline";
"widgets.controls.assist.title" = "Assist";
"widgets.controls.light.description" = "Turn on/off your light";
"widgets.controls.switch.description" = "Turn on/off your switch";
"widgets.controls.light.title" = "Light";
"widgets.controls.switch.title" = "Switch";
"widgets.controls.open_page.configuration.parameter.choose_page" = "Choose page";
"widgets.controls.open_page.configuration.parameter.page" = "Page";
"widgets.controls.open_page.configuration.title" = "Open Page";
Expand Down
58 changes: 24 additions & 34 deletions Sources/Extensions/AppIntents/Script/ScriptAppIntent.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import AppIntents
import AudioToolbox
import Foundation
import GRDB
import PromiseKit
import SFSafeSymbols
import Shared
import SwiftUI

Expand Down Expand Up @@ -70,6 +70,9 @@ final class ScriptAppIntent: AppIntent {
.Scripts.FailureMessage.content(script.displayString)
))
}

DataWidgetsUpdater.update()

return .result(value: success)
}
}
Expand Down Expand Up @@ -110,11 +113,11 @@ struct IntentScriptEntity: AppEntity {
@available(iOS 16.4, macOS 13.0, watchOS 9.0, *)
struct IntentScriptAppEntityQuery: EntityQuery, EntityStringQuery {
func entities(for identifiers: [String]) async throws -> [IntentScriptEntity] {
await getScriptEntities().flatMap(\.value).filter { identifiers.contains($0.id) }
getScriptEntities().flatMap(\.value).filter { identifiers.contains($0.id) }
}

func entities(matching string: String) async throws -> IntentItemCollection<IntentScriptEntity> {
let scriptsPerServer = await getScriptEntities()
let scriptsPerServer = getScriptEntities()

return .init(sections: scriptsPerServer.map { (key: Server, value: [IntentScriptEntity]) in
.init(
Expand All @@ -125,43 +128,30 @@ struct IntentScriptAppEntityQuery: EntityQuery, EntityStringQuery {
}

func suggestedEntities() async throws -> IntentItemCollection<IntentScriptEntity> {
let scriptsPerServer = await getScriptEntities()
let scriptsPerServer = getScriptEntities()

return .init(sections: scriptsPerServer.map { (key: Server, value: [IntentScriptEntity]) in
.init(.init(stringLiteral: key.info.name), items: value)
})
}

private func getScriptEntities(matching string: String? = nil) async -> [Server: [IntentScriptEntity]] {
await withCheckedContinuation { continuation in
var entities: [Server: [IntentScriptEntity]] = [:]
var serverCheckedCount = 0
for server in Current.servers.all.sorted(by: { $0.info.name < $1.info.name }) {
do {
let scripts: [HAAppEntity] = try Current.database().read { db in
try HAAppEntity
.filter(Column(DatabaseTables.AppEntity.serverId.rawValue) == server.identifier.rawValue)
.filter(Column(DatabaseTables.AppEntity.domain.rawValue) == Domain.script.rawValue)
.fetchAll(db)
}
entities[server] = scripts.map({ entity in
.init(
id: entity.id,
entityId: entity.entityId,
serverId: server.identifier.rawValue,
serverName: server.info.name,
displayString: entity.name,
iconName: entity.icon ?? ""
)
})
} catch {
Current.Log.error("Failed to load scripts from database: \(error.localizedDescription)")
}
serverCheckedCount += 1
if serverCheckedCount == Current.servers.all.count {
continuation.resume(returning: entities)
}
}
private func getScriptEntities(matching string: String? = nil) -> [Server: [IntentScriptEntity]] {
var scriptEntities: [Server: [IntentScriptEntity]] = [:]
let entities = ControlEntityProvider(domain: .script).getEntities(matching: string)

for (server, values) in entities {
scriptEntities[server] = values.map({ entity in
IntentScriptEntity(
id: entity.id,
entityId: entity.entityId,
serverId: entity.serverId,
serverName: server.info.name,
displayString: entity.name,
iconName: entity.icon ?? SFSymbol.applescriptFill.rawValue
)
})
}

return scriptEntities
}
}
10 changes: 8 additions & 2 deletions Sources/Extensions/Widgets/Light/ControlLight.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,17 @@ struct ControlLight: ControlWidget {
) { template in
ControlWidgetToggle(isOn: template.value, action: {
let intent = LightIntent()
intent.light = template.intentLightEntity
intent.light = .init(
id: template.id,
entityId: template.entityId,
serverId: template.serverId,
displayString: template.name,
iconName: template.icon.id
)
intent.value = !template.value
return intent
}()) {
Label(template.intentLightEntity.displayString, systemImage: template.icon.id)
Label(template.name, systemImage: template.icon.id)
}
.tint(.yellow)
}
Expand Down
Loading

0 comments on commit 4acaf55

Please sign in to comment.