iOS/Sources/Shared/API/Models/Action.swift

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
)
}
}