Files
iOS/Sources/Shared/API/Models/Action.swift
Bruno Pantaleão Gonçalves 4daad9416b Fix watch crash (Realm migration not called) (#2886)
<!-- Thank you for submitting a Pull Request and helping to improve Home
Assistant. Please complete the following sections to help the processing
and review of your changes. Please do not delete anything from this
template. -->

## Summary
<!-- Provide a brief summary of the changes you have made and most
importantly what they aim to achieve -->
For some reason Realm database migration was not being called event
though scheme version was already bumped to 28, apparently this was
caused by watch complication that was accessing realm passing specific
object types

Test process:

- Checkout commit c257fa21f6
- Run App in iPhone and watch (with some Actions created)
- Checkout commit 2c8470e5fe
- Run App in iPhone and watch
> - this was crashing in watch and now it is running the migration
block.

## Screenshots
<!-- If this is a user-facing change not in the frontend, please include
screenshots in light and dark mode. -->

## Link to pull request in Documentation repository
<!-- Pull requests that add, change or remove functionality must have a
corresponding pull request in the Companion App Documentation repository
(https://github.com/home-assistant/companion.home-assistant). Please add
the number of this pull request after the "#" -->
Documentation: home-assistant/companion.home-assistant#

## Any other notes
<!-- If there is any other information of note, like if this Pull
Request is part of a bigger change, please include it here. -->
2024-07-31 16:00:58 +02:00

275 lines
9.0 KiB
Swift

import Foundation
import ObjectMapper
import RealmSwift
import UIKit
public final class Action: Object, ImmutableMappable, UpdatableModel {
public enum PositionOffset: Int {
case manual = 0
case synced = 5000
case scene = 1_000_000
}
@objc public dynamic var ID: String = UUID().uuidString
@objc public dynamic var Name: String = ""
@objc public dynamic var Text: String = ""
@objc public dynamic var IconName: String = MaterialDesignIcons.allCases.randomElement()!.name
@objc public dynamic var BackgroundColor: String
@objc public dynamic var IconColor: String
@objc public dynamic var TextColor: String
@objc public dynamic var Position: Int = 0
@objc public dynamic var CreatedAt = Date()
@objc public dynamic var Scene: RLMScene?
@objc public dynamic var isServerControlled: Bool = false
@objc public dynamic var serverIdentifier: String = ""
@objc public dynamic var showInCarPlay: Bool = true
@objc public dynamic var showInWatch: Bool = true
@objc public dynamic var useCustomColors: Bool = false
static func primaryKey(sourceIdentifier: String, serverIdentifier: String) -> String {
#warning("multiserver - primary key duplication")
return sourceIdentifier
}
override public static func primaryKey() -> String? {
#keyPath(ID)
}
static func serverIdentifierKey() -> String {
#keyPath(serverIdentifier)
}
override public required init() {
let background = UIColor.randomBackgroundColor()
self.BackgroundColor = background.hexString()
if background.isLight {
self.TextColor = UIColor.black.hexString()
self.IconColor = UIColor.black.hexString()
} else {
self.TextColor = UIColor.white.hexString()
self.IconColor = UIColor.white.hexString()
}
super.init()
}
public func canConfigure(_ keyPath: PartialKeyPath<Action>) -> Bool {
if isServerControlled {
return false
}
switch keyPath {
case \Action.BackgroundColor:
return Scene == nil || Scene?.backgroundColor == nil
case \Action.TextColor:
return Scene == nil || Scene?.textColor == nil
case \Action.IconColor:
return Scene == nil || Scene?.iconColor == nil
case \Action.IconName,
\Action.Name,
\Action.Text:
return Scene == nil
case \Action.serverIdentifier:
return Scene == nil
case \Action.showInCarPlay:
return Scene == nil
case \Action.showInWatch:
return Scene == nil
case \Action.useCustomColors:
return Scene == nil
default:
return true
}
}
public required init(map: ObjectMapper.Map) throws {
// this is used for watch<->app syncing
self.ID = try map.value("ID")
self.Name = try map.value("Name")
self.Position = try map.value("Position")
self.BackgroundColor = try map.value("BackgroundColor")
self.IconName = try map.value("IconName")
self.IconColor = try map.value("IconColor")
self.Text = try map.value("Text")
self.TextColor = try map.value("TextColor")
self.CreatedAt = try map.value("CreatedAt", using: DateTransform())
self.isServerControlled = try map.value("isServerControlled")
self.serverIdentifier = try map.value("serverIdentifier")
self.showInCarPlay = try map.value("showInCarPlay")
self.showInWatch = try map.value("showInWatch")
self.useCustomColors = try map.value("useCustomColors")
super.init()
}
public func mapping(map: ObjectMapper.Map) {
ID >>> map["ID"]
Name >>> map["Name"]
Position >>> map["Position"]
BackgroundColor >>> map["BackgroundColor"]
IconName >>> map["IconName"]
IconColor >>> map["IconColor"]
Text >>> map["Text"]
TextColor >>> map["TextColor"]
CreatedAt >>> (map["CreatedAt"], DateTransform())
isServerControlled >>> map["isServerControlled"]
serverIdentifier >>> map["serverIdentifier"]
showInCarPlay >>> map["showInCarPlay"]
showInWatch >>> map["showInWatch"]
useCustomColors >>> map["useCustomColors"]
}
static func didUpdate(objects: [Action], server: Server, realm: Realm) {
for (idx, object) in objects.enumerated() {
object.Position = PositionOffset.synced.rawValue + server.info.sortOrder + idx
}
}
static func willDelete(objects: [Action], server: Server?, realm: Realm) {}
static var updateEligiblePredicate: NSPredicate {
.init(format: "isServerControlled == YES")
}
public func update(with object: MobileAppConfigAction, server: Server, using realm: Realm) -> Bool {
Current.Log.info("Updating server configured Actions")
if self.realm == nil {
ID = object.name
Name = object.name
} else {
precondition(ID == object.name)
precondition(Name == object.name)
}
isServerControlled = true
serverIdentifier = server.identifier.rawValue
Name = object.name
if let backgroundColor = object.backgroundColor {
BackgroundColor = backgroundColor
}
if let iconName = object.iconIcon {
IconName = iconName.normalizingIconString
} else {
let allCases = MaterialDesignIcons.allCases
IconName = allCases[abs(object.name.djb2hash % allCases.count)].name
}
if let iconColor = object.iconColor {
IconColor = iconColor
}
if let text = object.labelText {
Text = text
} else {
Text = object.name.replacingOccurrences(of: "_", with: " ").localizedCapitalized
}
if let textColor = object.labelColor {
TextColor = textColor
}
if let showInCarPlay = object.showInCarPlay {
self.showInCarPlay = showInCarPlay
}
if let showInWatch = object.showInWatch {
self.showInWatch = showInWatch
}
if let useCustomColors = object.useCustomColors {
self.useCustomColors = useCustomColors
}
return true
}
#if os(iOS)
public var uiShortcut: UIApplicationShortcutItem {
UIApplicationShortcutItem(
type: ID,
localizedTitle: Text,
localizedSubtitle: nil,
icon: nil,
userInfo: [:]
)
}
#endif
public enum TriggerType {
case event
case scene
}
public var triggerType: TriggerType {
// we don't sync the scene information over to the watch, so checking ID which is synced
if ID.starts(with: "scene.") {
return .scene
} else {
return .event
}
}
public func exampleTrigger(api: HomeAssistantAPI) -> String {
switch triggerType {
case .event:
let data = api.actionEvent(actionID: ID, actionName: Name, source: .Preview)
let eventDataStrings = data.eventData.map { $0 + ": " + $1 }.sorted()
let sourceStrings = HomeAssistantAPI.ActionSource.allCases.map(\.description).sorted()
let indentation = "\n "
return """
- platform: event
event_type: \(data.eventType)
event_data:
# source may be one of:
# - \(sourceStrings.joined(separator: indentation + "# - "))
\(eventDataStrings.joined(separator: indentation))
"""
case .scene:
let data = api.actionScene(actionID: ID, source: .Preview)
let eventDataStrings = data.serviceData.map { $0 + ": " + $1 }.sorted()
let indentation = "\n "
return """
# you can watch for the scene change
- platform: event
event_type: call_service
event_data:
domain: \(data.serviceDomain)
service: \(data.serviceName)
service_data:
\(eventDataStrings.joined(separator: indentation))
"""
}
}
public var widgetLinkURL: URL {
var components = URLComponents()
components.scheme = "homeassistant"
components.host = "perform_action"
components.path = "/" + ID
components.queryItems = [
.init(name: "source", value: HomeAssistantAPI.ActionSource.Widget.rawValue),
]
return components.url!
}
}
public extension UIColor {
static func randomBackgroundColor() -> UIColor {
// avoiding:
// - super gray (low saturation)
// - super black (low brightness)
// - super white (high brightness)
UIColor(
hue: CGFloat.random(in: 0 ... 1.0),
saturation: CGFloat.random(in: 0.5 ... 1.0),
brightness: CGFloat.random(in: 0.25 ... 0.75),
alpha: 1.0
)
}
}