├── .gitignore
├── .swift-version
├── .swiftformat
├── .swiftlint
├── BuildTools
└── SwiftFormat
│ └── swiftformat
├── Github-Info.xcconfig
├── Images
├── CCTV.PNG
├── CCTV.jpg
├── CCTV2.PNG
├── Car.PNG
├── Car2.PNG
├── Heater.PNG
├── Heater2.PNG
├── Heater3.PNG
├── Home.PNG
├── Home2.PNG
├── Icon.png
├── Lights.PNG
├── Vacuum.PNG
├── Vacuum2.PNG
└── Widget.jpg
├── IntelliNest-github.xctestplan
├── IntelliNest.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
└── xcshareddata
│ └── xcschemes
│ ├── IntelliNest-github.xcscheme
│ ├── IntelliNest-release.xcscheme
│ └── IntelliNest.xcscheme
├── IntelliNest
├── AppDelegate.swift
├── AppMain.swift
├── Errors
│ └── EntityError.swift
├── IntelliNest.xctestplan
├── Model
│ ├── CameraEntity.swift
│ ├── Coordinates.swift
│ ├── DoorState.swift
│ ├── Entity.swift
│ ├── EntityProtocol.swift
│ ├── HeaterEntity.swift
│ ├── HomeLocationEntity.swift
│ ├── InputNumberEntity.swift
│ ├── LightEntity.swift
│ ├── LockEntity.swift
│ ├── LockState.swift
│ ├── Lockable.swift
│ ├── NordPoolEntity.swift
│ ├── PurifierEntity.swift
│ ├── PurifierSpeed.swift
│ ├── QuickActionService.swift
│ ├── RoborockEntity.swift
│ ├── RoborockImageEntity.swift
│ ├── SlideableProtocol.swift
│ ├── SonnenEntity.swift
│ ├── SonnenStatusEntity.swift
│ ├── SwitchEntity.swift
│ ├── UserManager.swift
│ ├── Websocket
│ │ ├── CallScriptRequest.swift
│ │ ├── CallServiceRequest.swift
│ │ ├── ServiceData.swift
│ │ ├── SubscribeRequest.swift
│ │ └── UpdateEntityRequest.swift
│ ├── YaleLock.swift
│ └── YaleLockResponse.swift
├── Navigation
│ ├── Destination.swift
│ ├── Navigator.swift
│ ├── NavigatorHelpers.swif
│ ├── NavigatorHelpers.swift
│ ├── ToolbarBackButton.swift
│ └── ToolbarItems.swift
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── Services
│ ├── GeofenceManager.swift
│ ├── NetworkTask.swift
│ ├── NotificationService.swift
│ ├── PreviewProvideUtil.swift
│ ├── RestAPIService.swift
│ ├── URLCreator.swift
│ ├── URLRequestBuilder.swift
│ └── YaleApiService.swift
├── Supporting Files
│ ├── AppIntentVocabulary.plist
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── App Store.png
│ │ │ ├── Contents.json
│ │ │ ├── iPad App Icon iOS 7,12 1.png
│ │ │ ├── iPad App Icon iOS 7,12@2x.png
│ │ │ ├── iPad Pro Icon iOS 9,12@2x.png
│ │ │ ├── iPad Settings 1.png
│ │ │ ├── iPad Settings.png
│ │ │ ├── iPad Settings@2x 1.png
│ │ │ ├── iPad Settings@2x.png
│ │ │ ├── iPad Spotlight iOS 7,12.png
│ │ │ ├── iPad Spotlight iOS 7,12@2x 1.png
│ │ │ ├── iPad Spotlight iOS 7,12@2x.png
│ │ │ ├── iPad notification iOS 7,12.png
│ │ │ ├── iPad notification iOS 7,12@2x.png
│ │ │ ├── iPhone App Icon iOS 5,6.png
│ │ │ ├── iPhone App Icon iOS 5,6@2x.png
│ │ │ ├── iPhone App Icon iOS 7,12@2x.png
│ │ │ ├── iPhone App Icon iOS 7,12@3x.png
│ │ │ ├── iPhone Spotlight iOS 5,6 & Settings@3x.png
│ │ │ ├── iPhone Spotlight iOS 7,12@3x.png
│ │ │ ├── iPhone notification iOS 7,12@2x.png
│ │ │ └── iPhone notification iOS 7,12@3x.png
│ │ ├── Contents.json
│ │ ├── Images.imageset
│ │ │ └── Contents.json
│ │ ├── aircondition.imageset
│ │ │ ├── Contents.json
│ │ │ └── heater.png
│ │ ├── defrost.filled.imageset
│ │ │ ├── Contents.json
│ │ │ └── defrost.filled.png
│ │ ├── ev-plug-ccs2.imageset
│ │ │ ├── Contents.json
│ │ │ └── ev-plug-ccs2.png
│ │ ├── ev-plug-type2.imageset
│ │ │ ├── Contents.json
│ │ │ └── ev-plug-type2.png
│ │ ├── floorplan.imageset
│ │ │ ├── Contents.json
│ │ │ └── floorplan.png
│ │ ├── gym.imageset
│ │ │ ├── Contents.json
│ │ │ └── gym.png
│ │ ├── hallway.imageset
│ │ │ ├── Contents.json
│ │ │ └── hallway.png
│ │ ├── powergrid.imageset
│ │ │ ├── Contents.json
│ │ │ └── powergrid.png
│ │ ├── refresh.imageset
│ │ │ ├── Contents.json
│ │ │ └── icons8-iOS Glyph-tovbiOioOGAO-50-ffffff.png
│ │ ├── roborocks7.imageset
│ │ │ ├── Contents.json
│ │ │ └── icon_128x128.png
│ │ ├── seatheater.filled.imageset
│ │ │ ├── Contents.json
│ │ │ └── seatheater.filled.png
│ │ ├── settings.imageset
│ │ │ ├── Contents.json
│ │ │ └── settings.png
│ │ ├── solarpanel.imageset
│ │ │ ├── Contents.json
│ │ │ └── solarpanel.png
│ │ ├── vince.imageset
│ │ │ ├── Contents.json
│ │ │ └── vince.png
│ │ └── washing.imageset
│ │ │ ├── Contents.json
│ │ │ └── washing.png
│ ├── Info.plist
│ └── IntelliNest.entitlements
├── Utils
│ ├── Constants
│ │ ├── Actions.swift
│ │ ├── Closures.swift
│ │ ├── DeviceID.swift
│ │ ├── Domain.swift
│ │ ├── EntityId.swift
│ │ ├── FullScreenBackgroundOverlay.swift
│ │ ├── GlobalConstants.swift
│ │ ├── HTTPMethod.swift
│ │ ├── Heater.swift
│ │ ├── JSONKey.swift
│ │ ├── LockID.swift
│ │ ├── ScriptID.swift
│ │ ├── ScriptVariableKeys.swift
│ │ ├── ServiceDataKeys.swift
│ │ ├── ServiceID.swift
│ │ ├── ServiceTargetKeys.swift
│ │ ├── ServiceValues.swift
│ │ └── StorageKeys.swift
│ ├── Extensions
│ │ ├── CalendarExtension.swift
│ │ ├── CollectionExtension.swift
│ │ ├── ColorExtension.swift
│ │ ├── DateExtension.swift
│ │ ├── DoubleExtension.swift
│ │ ├── EncodableExtension.swift
│ │ ├── FontExtension.swift
│ │ ├── HomeViewModelExtension.swift
│ │ ├── ImageExtension.swift
│ │ ├── LinearGradientExtension.swift
│ │ ├── LogExtension.swift
│ │ ├── StringExtension.swift
│ │ ├── TaskExtension.swift
│ │ ├── UINavigationController.swift
│ │ ├── UserDefaultsExtension.swift
│ │ ├── UserDefaultsExtensionShared.swift
│ │ ├── ViewExtension.swift
│ │ └── ViewModels
│ │ │ └── HomeViewModelExtension+LockServiceProtocol.swift
│ └── SSIDUtil.swift
├── ViewModels
│ ├── ElectricityViewModel.swift
│ ├── HeatersViewModel.swift
│ ├── HeatersViewModelExtension.swift
│ ├── HomeViewModel.swift
│ ├── LightsViewModel.swift
│ ├── LockServiceProtocol.swift
│ ├── LynkViewModel.swift
│ ├── LynkViewModelHelpers.swift
│ ├── RoborockViewModel.swift
│ └── VLCMediaplayerMock.swift
└── Views
│ ├── Components
│ ├── BatteryView.swift
│ ├── DashboardButtonView.swift
│ ├── ErrorBannerView.swift
│ ├── FuelView.swift
│ ├── INText.swift
│ ├── NavigationButtonView.swift
│ ├── NumberPickerScrollView.swift
│ ├── PrimaryContentBorderView.swift
│ ├── ServiceButtonView.swift
│ ├── VerticalDivider.swift
│ └── ZoomableImageView.swift
│ ├── Dashboards
│ ├── ElectricityView.swift
│ ├── HeatersView.swift
│ ├── HomeView.swift
│ ├── LeafView.swift
│ ├── LightsView.swift
│ ├── LynkView.swift
│ └── RoborockView.swift
│ ├── Electricity
│ ├── ElectricityFlowView.swift
│ ├── FlowIndicatorView.swift
│ ├── NordPoolHistoryView.swift
│ └── SonnenSettingsView.swift
│ ├── Heater
│ ├── DetailedHeaterView.swift
│ ├── FanModeButtonView.swift
│ ├── FanModeView.swift
│ ├── HorizontalButtonView.swift
│ ├── HorizontalModeView.swift
│ ├── HvacModeView.swift
│ ├── NumberTextView.swift
│ ├── PurifierView.swift
│ ├── SimpleHeaterView.swift
│ ├── ThermometerGroupView.swift
│ ├── ThermometerView.swift
│ ├── VerticalButtonView.swift
│ └── VerticalPositionView.swift
│ ├── Home
│ └── CoffeeMachineSchedulingView.swift
│ ├── Labels
│ └── HeaterGroupLabels.swift
│ ├── Light
│ ├── BulbButton.swift
│ ├── DualBulbRoomView.swift
│ ├── SingleRoomLight.swift
│ └── VerticalSlider.swift
│ ├── Lynk
│ ├── DateTimePickerView.swift
│ ├── LimitPickerView.swift
│ └── LynkHeaterOptionsView.swift
│ └── Roborock
│ ├── RoborockInfoView.swift
│ ├── RoborockMainButtons.swift
│ ├── RoborockMapImageView.swift
│ ├── RoborockRoomsView.swift
│ └── SinceEmptiedView.swift
├── IntelliNestTests
├── EntityTest.swift
├── InputNumberEntityTest.swift
├── LightEntityTests.swift
├── LockEntityTest.swift
└── URLProtocolStub.swift
├── IntelliWidget
├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ ├── Contents.json
│ ├── WidgetBackground.colorset
│ │ └── Contents.json
│ └── widget-home-icon.imageset
│ │ ├── Contents.json
│ │ └── widget-home-icon.png
├── CarHeaterWidget.swift
├── HomeWidget.swift
├── Info.plist
├── IntelliWidgetBundle.swift
└── IntelliWidgetExtension.entitlements
├── LICENSE
├── README.md
├── Scripts
└── generate_code_stats.zsh
└── buildServer.json
/.gitignore:
--------------------------------------------------------------------------------
1 | IntelliNest.xcodeproj/project.xcworkspace/xcuserdata/*
2 | IntelliNest.xcodeproj/xcuserdata/tobias.xcuserdatad/xcschemes/xcschememanagement.plist
3 | .xcbkptlist
4 | xcdebugger
5 | .DS_Store
6 | IntelliNest-Info.xcconfig
7 | # CocoaPods
8 | Pods/
9 | .nvim/
10 | buildserver.json
11 |
--------------------------------------------------------------------------------
/.swift-version:
--------------------------------------------------------------------------------
1 | 5.10
2 |
--------------------------------------------------------------------------------
/.swiftformat:
--------------------------------------------------------------------------------
1 | --exclude Carthage,.build,vendor
2 | --commas inline
3 | --disable specifiers, wrapMultilineStatementBraces, fileHeader, andOperator, unusedArguments
4 | --header strip
5 |
--------------------------------------------------------------------------------
/.swiftlint:
--------------------------------------------------------------------------------
1 | line_length:
2 | warning: 140
3 | error: 160
4 | disabled_rules:
5 | - blanket_disable_command
6 | - nesting
7 | excluded: # paths to ignore during linting. Takes precedence over `included`.
8 | - Pods
9 | - R.generated.swift
10 | - .build # Where Swift Package Manager checks out dependency sources
11 |
--------------------------------------------------------------------------------
/BuildTools/SwiftFormat/swiftformat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/BuildTools/SwiftFormat/swiftformat
--------------------------------------------------------------------------------
/Github-Info.xcconfig:
--------------------------------------------------------------------------------
1 | //
2 | // Github-Info.xcconfig
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-05-22.
6 | //
7 |
8 | // Configuration settings file format documentation can be found at:
9 | // https://help.apple.com/xcode/#/dev745c5c974
10 | SECRET_HASS_TOKEN = default_token
11 | SECRET_HASS_TOKEN_SARAH = default_token_sarah
12 | EXTERNAL_URL = 192.218.223.123/
13 | LOCAL_SSID = None
14 | SECRET_SHIP_BOOK_APP_ID = appID
15 | SECRET_SHIP_BOOK_APP_KEY = appKey
16 | SECRET_YALE_API_KEY = abc
17 | SECRET_YALE_API_URL = abc.com
18 | SECRET_RTSP_STREAM_CAMERA_BACK = 192.168.5.12
19 | SECRET_RTSP_STREAM_CAMERA_FRONT = 192.168.5.12
20 | SECRET_RTSP_STREAM_CAMERA_CARPORT = 192.168.5.12
21 | SECRET_RTSP_STREAM_CAMERA_VINCE = 192.168.5.12
22 |
--------------------------------------------------------------------------------
/Images/CCTV.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/Images/CCTV.PNG
--------------------------------------------------------------------------------
/Images/CCTV.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/Images/CCTV.jpg
--------------------------------------------------------------------------------
/Images/CCTV2.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/Images/CCTV2.PNG
--------------------------------------------------------------------------------
/Images/Car.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/Images/Car.PNG
--------------------------------------------------------------------------------
/Images/Car2.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/Images/Car2.PNG
--------------------------------------------------------------------------------
/Images/Heater.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/Images/Heater.PNG
--------------------------------------------------------------------------------
/Images/Heater2.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/Images/Heater2.PNG
--------------------------------------------------------------------------------
/Images/Heater3.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/Images/Heater3.PNG
--------------------------------------------------------------------------------
/Images/Home.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/Images/Home.PNG
--------------------------------------------------------------------------------
/Images/Home2.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/Images/Home2.PNG
--------------------------------------------------------------------------------
/Images/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/Images/Icon.png
--------------------------------------------------------------------------------
/Images/Lights.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/Images/Lights.PNG
--------------------------------------------------------------------------------
/Images/Vacuum.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/Images/Vacuum.PNG
--------------------------------------------------------------------------------
/Images/Vacuum2.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/Images/Vacuum2.PNG
--------------------------------------------------------------------------------
/Images/Widget.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/Images/Widget.jpg
--------------------------------------------------------------------------------
/IntelliNest-github.xctestplan:
--------------------------------------------------------------------------------
1 | {
2 | "configurations" : [
3 | {
4 | "id" : "CEB6A170-3D07-4808-9525-DC1115BD03E5",
5 | "name" : "Configuration 1",
6 | "options" : {
7 |
8 | }
9 | }
10 | ],
11 | "defaultOptions" : {
12 | "codeCoverage" : false,
13 | "targetForVariableExpansion" : {
14 | "containerPath" : "container:IntelliNest.xcodeproj",
15 | "identifier" : "F67023F627A177320070FFBB",
16 | "name" : "IntelliNest"
17 | }
18 | },
19 | "testTargets" : [
20 | {
21 | "parallelizable" : true,
22 | "target" : {
23 | "containerPath" : "container:IntelliNest.xcodeproj",
24 | "identifier" : "F670240627A177340070FFBB",
25 | "name" : "IntelliNestTests"
26 | }
27 | }
28 | ],
29 | "version" : 1
30 | }
31 |
--------------------------------------------------------------------------------
/IntelliNest.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/IntelliNest.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/IntelliNest.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "66d0e924887faeee4da8d3368133bc4c63f99389594ed32f1ce043232531e4e8",
3 | "pins" : [
4 | {
5 | "identity" : "shipbooksdk-ios",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/ShipBook/ShipBookSDK-iOS.git",
8 | "state" : {
9 | "revision" : "20864a7f61064c541d73b5adaa781e5029e56bc7",
10 | "version" : "1.1.20"
11 | }
12 | },
13 | {
14 | "identity" : "starscream",
15 | "kind" : "remoteSourceControl",
16 | "location" : "https://github.com/daltoniam/Starscream",
17 | "state" : {
18 | "revision" : "c6bfd1af48efcc9a9ad203665db12375ba6b145a",
19 | "version" : "4.0.8"
20 | }
21 | }
22 | ],
23 | "version" : 3
24 | }
25 |
--------------------------------------------------------------------------------
/IntelliNest/Errors/EntityError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EntityError.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-07-07.
6 | //
7 |
8 | import Foundation
9 |
10 | enum EntityError: Error {
11 | case updateTooEarly
12 | case badRequest
13 | case badResponse
14 | case badImageData
15 | case httpRequestFailure
16 | case genericError
17 | }
18 |
--------------------------------------------------------------------------------
/IntelliNest/IntelliNest.xctestplan:
--------------------------------------------------------------------------------
1 | {
2 | "configurations" : [
3 | {
4 | "id" : "1694E45D-B5AE-4849-B0E6-FAA62C5C8D8A",
5 | "name" : "Configuration 1",
6 | "options" : {
7 |
8 | }
9 | }
10 | ],
11 | "defaultOptions" : {
12 | "codeCoverage" : {
13 | "targets" : [
14 | {
15 | "containerPath" : "container:IntelliNest.xcodeproj",
16 | "identifier" : "F67023F627A177320070FFBB",
17 | "name" : "IntelliNest"
18 | }
19 | ]
20 | },
21 | "environmentVariableEntries" : [
22 | {
23 | "key" : "CFNETWORK_DIAGNOSTICS",
24 | "value" : "0"
25 | }
26 | ],
27 | "targetForVariableExpansion" : {
28 | "containerPath" : "container:IntelliNest.xcodeproj",
29 | "identifier" : "F67023F627A177320070FFBB",
30 | "name" : "IntelliNest"
31 | }
32 | },
33 | "testTargets" : [
34 | {
35 | "parallelizable" : true,
36 | "target" : {
37 | "containerPath" : "container:IntelliNest.xcodeproj",
38 | "identifier" : "F670240627A177340070FFBB",
39 | "name" : "IntelliNestTests"
40 | }
41 | }
42 | ],
43 | "version" : 1
44 | }
45 |
--------------------------------------------------------------------------------
/IntelliNest/Model/CameraEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraEntity.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-06-18.
6 | //
7 |
8 | import Foundation
9 |
10 | struct CameraEntity: EntityProtocol {
11 | var entityId: EntityId
12 | var state: String
13 | var nextUpdate = Date().addingTimeInterval(-1)
14 | var isActive: Bool = false
15 | var isLoading: Bool = false
16 | // var imageUrlString: String = ""
17 | var urlPath = ""
18 |
19 | enum CodingKeys: String, CodingKey {
20 | case entityId = "entity_id"
21 | case state
22 | case attributes
23 | }
24 |
25 | init(entityId: EntityId, state: String = "Loading") {
26 | self.entityId = entityId
27 | self.state = state
28 | }
29 |
30 | init(from decoder: Decoder) throws {
31 | let container = try decoder.container(keyedBy: CodingKeys.self)
32 | let entityId = try EntityId(rawValue: container.decode(String.self, forKey: .entityId))
33 | self.entityId = entityId ?? EntityId.unknown
34 | state = try container.decode(String.self, forKey: .state)
35 |
36 | let attributes = try container.decode(CameraAttributes.self, forKey: .attributes)
37 | urlPath = attributes.urlPath
38 | }
39 |
40 | mutating func setNextUpdateTime() {
41 | nextUpdate = Date().addingTimeInterval(-1)
42 | }
43 |
44 | mutating func updateIsActive() {
45 | isActive.toggle()
46 | }
47 |
48 | static func == (lhs: CameraEntity, rhs: CameraEntity) -> Bool {
49 | false
50 | }
51 | }
52 |
53 | struct CameraAttributes: Decodable {
54 | var urlPath: String
55 |
56 | enum CodingKeys: String, CodingKey {
57 | case urlPath = "entity_picture"
58 | }
59 |
60 | init(from decoder: Decoder) throws {
61 | let container = try decoder.container(keyedBy: CodingKeys.self)
62 | urlPath = try container.decode(String.self, forKey: .urlPath)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/IntelliNest/Model/Coordinates.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Coordinates.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2024-01-24.
6 | //
7 |
8 | import CoreLocation
9 | import Foundation
10 |
11 | struct Coordinates: Codable, Hashable {
12 | let longitude: Double
13 | let latitude: Double
14 |
15 | func toCLLocationCoordinate2D() -> CLLocationCoordinate2D {
16 | CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/IntelliNest/Model/DoorState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DoorState.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-05-18.
6 | //
7 |
8 | import Foundation
9 |
10 | enum DoorState: String, Decodable {
11 | case open
12 | case closed
13 | case unknown
14 | }
15 |
--------------------------------------------------------------------------------
/IntelliNest/Model/EntityProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EntityProtocol.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-08-08.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol EntityProtocol: Decodable, Equatable {
11 | var entityId: EntityId { get }
12 | var state: String { get set }
13 | var nextUpdate: Date { get set }
14 | var isActive: Bool { get }
15 |
16 | mutating func setNextUpdateTime()
17 | func canUpdate() -> Bool
18 | }
19 |
20 | extension EntityProtocol {
21 | func canUpdate() -> Bool {
22 | if nextUpdate.timeIntervalSinceNow < 0 {
23 | true
24 | } else {
25 | false
26 | }
27 | }
28 |
29 | mutating func setNextUpdateTime() {
30 | nextUpdate = Date().addingTimeInterval(0.5)
31 | }
32 | }
33 |
34 | struct EntityMinimized: Decodable {
35 | let state: String
36 |
37 | enum CodingKeys: String, CodingKey {
38 | case state
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/IntelliNest/Model/HomeLocationEntity.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct HomeLocationEntity: EntityProtocol {
4 | let entityId: EntityId
5 | var state: String
6 | var latitude: Double
7 | var longitude: Double
8 | var nextUpdate = Date.now
9 | var isActive = false
10 |
11 | enum CodingKeys: String, CodingKey {
12 | case entityId = "entity_id"
13 | case state
14 | case attributes
15 | }
16 |
17 | init(from decoder: Decoder) throws {
18 | let container = try decoder.container(keyedBy: CodingKeys.self)
19 | let entityId = try EntityId(rawValue: container.decode(String.self, forKey: .entityId))
20 | self.entityId = entityId ?? EntityId.unknown
21 | state = try container.decode(String.self, forKey: .state)
22 |
23 | let attributes = try container.decode(HomeLocationAttributes.self, forKey: .attributes)
24 | latitude = attributes.latitude
25 | longitude = attributes.longitude
26 | }
27 | }
28 |
29 | struct HomeLocationAttributes: Decodable {
30 | let latitude: Double
31 | let longitude: Double
32 | }
33 |
--------------------------------------------------------------------------------
/IntelliNest/Model/InputNumberEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InputNumberEntity.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-03-28.
6 | //
7 |
8 | import Foundation
9 |
10 | struct InputNumberEntity: EntityProtocol {
11 | var entityId: EntityId
12 | var state: String { didSet {
13 | inputNumber = Double(state) ?? 0
14 | }}
15 | var lastUpdated: Date
16 | var lastChanged: Date
17 | var nextUpdate = Date().addingTimeInterval(-1)
18 | var isActive = false
19 | var isLoading = false
20 | var inputNumber: Double = 0
21 |
22 | enum CodingKeys: String, CodingKey {
23 | case entityId = "entity_id"
24 | case state
25 | case lastChanged = "last_changed"
26 | case lastUpdated = "last_updated"
27 | }
28 |
29 | init(entityId: EntityId, state: String = "Loading") {
30 | self.entityId = entityId
31 | self.state = state
32 | self.lastChanged = .distantPast
33 | self.lastUpdated = .distantPast
34 | }
35 |
36 | static let numberFormat: NumberFormatter = {
37 | let numberFormatter = NumberFormatter()
38 | numberFormatter.minimumFractionDigits = 0
39 | numberFormatter.maximumFractionDigits = 1
40 | return numberFormatter
41 | }()
42 |
43 | init(from decoder: Decoder) throws {
44 | let container = try decoder.container(keyedBy: CodingKeys.self)
45 | let entityId = try EntityId(rawValue: container.decode(String.self, forKey: .entityId))
46 | self.entityId = entityId ?? EntityId.unknown
47 | state = try container.decode(String.self, forKey: .state)
48 | inputNumber = try Double(container.decode(String.self, forKey: .state)) ?? 22
49 |
50 | let dateFormatter = DateFormatter()
51 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ"
52 | let lastChanged = try container.decode(String.self, forKey: .lastChanged)
53 | self.lastChanged = dateFormatter.date(from: lastChanged) ?? .distantPast
54 | let lastUpdated = try container.decode(String.self, forKey: .lastUpdated)
55 | self.lastUpdated = dateFormatter.date(from: lastUpdated) ?? .distantPast
56 | }
57 |
58 | mutating func updateIsActive() {
59 | isActive = false
60 | }
61 |
62 | mutating func setNextUpdateTime() {
63 | nextUpdate = Date().addingTimeInterval(0.29)
64 | }
65 |
66 | static func == (lhs: InputNumberEntity, rhs: InputNumberEntity) -> Bool {
67 | lhs.entityId == rhs.entityId && lhs.state == rhs.state
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/IntelliNest/Model/LightEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LightEntity.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-06-30.
6 | //
7 |
8 | import Foundation
9 |
10 | struct LightEntity: EntityProtocol, Decodable {
11 | var entityId: EntityId
12 | var state: String
13 | var nextUpdate = Date().addingTimeInterval(-1)
14 | var isActive: Bool {
15 | state == "on" ? true : false
16 | }
17 |
18 | var isSliding = false
19 | var isUpdating = false
20 |
21 | var brightness: Int
22 | var groupedLightIDs: [EntityId]?
23 |
24 | enum CodingKeys: String, CodingKey {
25 | case entityId = "entity_id"
26 | case state
27 | case attributes
28 | }
29 |
30 | init(entityId: EntityId, state: String = "Loading", groupedLightIDs: [EntityId]? = nil) {
31 | self.entityId = entityId
32 | self.state = state
33 | brightness = -1
34 | self.groupedLightIDs = groupedLightIDs
35 | }
36 |
37 | init(from decoder: Decoder) throws {
38 | let container = try decoder.container(keyedBy: CodingKeys.self)
39 | entityId = try container.decode(EntityId.self, forKey: .entityId)
40 | state = try container.decode(String.self, forKey: .state)
41 |
42 | if let attributesContainer = try? container.nestedContainer(keyedBy: AttributesCodingKeys.self, forKey: .attributes) {
43 | brightness = try attributesContainer.decodeIfPresent(Int.self, forKey: .brightness) ?? -1
44 | } else {
45 | brightness = -1
46 | }
47 | groupedLightIDs = nil
48 | }
49 |
50 | mutating func updateIsActive() {}
51 |
52 | private enum AttributesCodingKeys: String, CodingKey {
53 | case brightness
54 | }
55 |
56 | private struct Attributes: Decodable {
57 | var brightness: Int
58 |
59 | init(from decoder: Decoder) throws {
60 | let data = try decoder.container(keyedBy: AttributesCodingKeys.self)
61 | brightness = try data.decodeIfPresent(Int.self, forKey: .brightness) ?? -1
62 | }
63 | }
64 |
65 | mutating func setNextUpdateTime() {
66 | nextUpdate = Date().addingTimeInterval(0.5)
67 | }
68 |
69 | static func == (lhs: LightEntity, rhs: LightEntity) -> Bool {
70 | lhs.entityId == rhs.entityId &&
71 | lhs.brightness == rhs.brightness &&
72 | lhs.isActive == rhs.isActive &&
73 | lhs.state == rhs.state
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/IntelliNest/Model/LockEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LockEntity.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-04-19.
6 | //
7 |
8 | import Foundation
9 | import ShipBookSDK
10 |
11 | struct LockEntity: Lockable, EntityProtocol {
12 | var entityId: EntityId
13 | var id: LockID {
14 | switch entityId {
15 | case .storageLock:
16 | .storageDoor
17 | case .lynkDoorLock:
18 | .lynkDoor
19 | default:
20 | .storageDoor
21 | }
22 | }
23 |
24 | var state: String { didSet {
25 | lockState = LockState(rawValue: state) ?? .unknown
26 | if lockState == expectedState || expectedStateIsOld {
27 | expectedState = .unknown
28 | }
29 | }}
30 | var nextUpdate = Date().addingTimeInterval(-1)
31 | var expectedStateSetDate: Date?
32 |
33 | var lockState: LockState = .unknown
34 | var expectedState: LockState = .unknown {
35 | didSet {
36 | self.expectedStateSetDate = Date()
37 | }
38 | }
39 |
40 | enum CodingKeys: String, CodingKey {
41 | case entityId = "entity_id"
42 | case state
43 | }
44 |
45 | init(entityId: EntityId, state: String = "Loading") {
46 | self.entityId = entityId
47 | self.state = state
48 | }
49 |
50 | init(from decoder: Decoder) throws {
51 | let container = try decoder.container(keyedBy: CodingKeys.self)
52 | let entityId = try EntityId(rawValue: container.decode(String.self, forKey: .entityId))
53 | self.entityId = entityId ?? EntityId.unknown
54 | state = try container.decode(String.self, forKey: .state)
55 | lockState = LockState(rawValue: state) ?? .unknown
56 | }
57 |
58 | static func == (lhs: LockEntity, rhs: LockEntity) -> Bool {
59 | lhs.isActive == rhs.isActive &&
60 | lhs.entityId == rhs.entityId &&
61 | lhs.state == rhs.state
62 | }
63 |
64 | func shouldReload() -> Bool {
65 | isActive || isLoading
66 | }
67 |
68 | mutating func setNextUpdateTime() {
69 | nextUpdate = Date().addingTimeInterval(0.29)
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/IntelliNest/Model/LockState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LockState.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-05-18.
6 | //
7 |
8 | import Foundation
9 |
10 | enum LockState: String, Decodable {
11 | case locked
12 | case locking
13 | case unlocked
14 | case unlocking
15 | case unknown
16 | }
17 |
--------------------------------------------------------------------------------
/IntelliNest/Model/Lockable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Lockable.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-05-18.
6 | //
7 |
8 | import SwiftUI
9 |
10 | protocol Lockable {
11 | var id: LockID { get }
12 | var lockState: LockState { get set }
13 | var isLoading: Bool { get }
14 | var actionText: String { get }
15 | var expectedState: LockState { get set }
16 | var expectedStateSetDate: Date? { get set }
17 | var expectedStateIsOld: Bool { get }
18 | var isActive: Bool { get }
19 | var image: Image { get }
20 | func stateToString() -> String
21 | }
22 |
23 | extension Lockable {
24 | var actionText: String {
25 | switch lockState {
26 | case .unlocked:
27 | "Lås"
28 | case .unlocking:
29 | "Låser upp"
30 | case .locking:
31 | "Låser"
32 | case .locked:
33 | "Lås upp"
34 | default:
35 | "Lås upp"
36 | }
37 | }
38 |
39 | var isLoading: Bool {
40 | (expectedState != .unknown && lockState != expectedState) || (lockState == .unknown && !expectedStateIsOld)
41 | }
42 |
43 | var isActive: Bool {
44 | [.unlocked, .unlocking, .locking].contains(lockState)
45 | }
46 |
47 | var expectedStateIsOld: Bool {
48 | if let setDate = expectedStateSetDate {
49 | Date().timeIntervalSince(setDate) > 30
50 | } else {
51 | false
52 | }
53 | }
54 |
55 | var image: Image {
56 | if lockState == .locked || lockState == .unlocking {
57 | Image(systemImageName: .locked)
58 | } else if lockState == .unlocked || lockState == .locking {
59 | Image(systemImageName: .unlocked)
60 | } else {
61 | Image(systemImageName: .lockSlash)
62 | }
63 | }
64 |
65 | func stateToString() -> String {
66 | switch lockState {
67 | case .locked:
68 | "låst"
69 | case .unlocked:
70 | "olåst"
71 | case .unlocking:
72 | "låser upp"
73 | case .locking:
74 | "låser"
75 | default:
76 | lockState.rawValue
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/IntelliNest/Model/PurifierEntity.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum PurifierFanMode: String, Decodable {
4 | case off
5 | case manual
6 | case auto
7 | }
8 |
9 | struct PurifierEntity: Decodable {
10 | var fanMode: PurifierFanMode = .off
11 | var speed: Double = 0
12 | var temperature: Double = 0
13 | var humidity: Int = 0
14 | var isActive: Bool {
15 | fanMode != .off
16 | }
17 |
18 | init() {}
19 | }
20 |
--------------------------------------------------------------------------------
/IntelliNest/Model/PurifierSpeed.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct PurifierSpeed: Decodable, EntityProtocol {
4 | var entityId = EntityId.purifierFanSpeed
5 | var state = ""
6 | var nextUpdate = Date.now
7 | var isActive = false
8 | var speed: Double
9 |
10 | enum CodingKeys: String, CodingKey {
11 | case entityId = "entity_id"
12 | case state
13 | case attributes
14 | }
15 |
16 | init(from decoder: Decoder) throws {
17 | let container = try decoder.container(keyedBy: CodingKeys.self)
18 | let entityId = try EntityId(rawValue: container.decode(String.self, forKey: .entityId))
19 | self.entityId = entityId ?? EntityId.unknown
20 | state = try container.decode(String.self, forKey: .state)
21 |
22 | let attributes = try container.decode(PurifierSpeedAttributes.self, forKey: .attributes)
23 | speed = attributes.percentage.toFanSpeedTargetNumber
24 | }
25 | }
26 |
27 | struct PurifierSpeedAttributes: Decodable {
28 | let percentage: Double
29 | }
30 |
--------------------------------------------------------------------------------
/IntelliNest/Model/QuickActionService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QuickActionService.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-06-20.
6 | //
7 |
8 | import UIKit
9 |
10 | // 1
11 | enum QuickActionType: String {
12 | case carHeater
13 | }
14 |
15 | // 2
16 | enum QuickAction: Equatable {
17 | case carheater
18 |
19 | // 3
20 | init?(shortcutItem: UIApplicationShortcutItem) {
21 | // 4
22 | guard let type = QuickActionType(rawValue: shortcutItem.type) else {
23 | return nil
24 | }
25 |
26 | // 5
27 | switch type {
28 | case .carHeater:
29 | self = .carheater
30 | }
31 | }
32 | }
33 |
34 | // 6
35 | class QuickActionService: ObservableObject {
36 | @MainActor static let shared = QuickActionService()
37 |
38 | // 7
39 | @Published var action: QuickAction?
40 | }
41 |
--------------------------------------------------------------------------------
/IntelliNest/Model/RoborockEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoborockEntity.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-04-25.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct RoborockEntity: EntityProtocol {
11 | let entityId: EntityId
12 | var state: String
13 |
14 | var nextUpdate = Date().addingTimeInterval(-1)
15 | var isActive: Bool {
16 | isCleaning || isReturning
17 | }
18 |
19 | var isCleaning: Bool {
20 | ["cleaning", "segment cleaning"].contains { $0 == state.lowercased() || $0 == status.lowercased() }
21 | }
22 |
23 | var isReturning: Bool {
24 | ["returning home", "returning"].contains { $0 == state.lowercased() || $0 == status.lowercased() }
25 | }
26 |
27 | var cleanIcon: Image {
28 | isCleaning ? .init(systemImageName: .pause) : .init(systemImageName: .play)
29 | }
30 |
31 | var cleanButtonTitle: String {
32 | isCleaning ? "Pausa" : "Dammsug"
33 | }
34 |
35 | var returningIcon: Image {
36 | isReturning ? .init(systemImageName: .pause) : .init(systemImageName: .house)
37 | }
38 |
39 | var returnButtonTitle: String {
40 | isReturning ? "Pausa" : "Docka"
41 | }
42 |
43 | var status: String = ""
44 |
45 | var batteryLevel: Int = -1
46 | var error: String = ""
47 |
48 | enum CodingKeys: String, CodingKey {
49 | case entityId = "entity_id"
50 | case state
51 | case attributes
52 | }
53 |
54 | init(entityId: EntityId, state: String = "Loading") {
55 | self.entityId = entityId
56 | self.state = state
57 | }
58 |
59 | init(from decoder: Decoder) throws {
60 | let container = try decoder.container(keyedBy: CodingKeys.self)
61 | let entityId = try EntityId(rawValue: container.decode(String.self, forKey: .entityId))
62 | self.entityId = entityId ?? EntityId.unknown
63 | state = try container.decode(String.self, forKey: .state)
64 |
65 | let attributes = try container.decode(RoborockAttributes.self, forKey: .attributes)
66 | batteryLevel = attributes.batteryLevel
67 | }
68 | }
69 |
70 | private struct RoborockAttributes: Decodable {
71 | var batteryLevel: Int
72 |
73 | private enum CodingKeys: String, CodingKey {
74 | case batteryLevel = "battery_level"
75 | }
76 |
77 | init(from decoder: Decoder) throws {
78 | let container = try decoder.container(keyedBy: CodingKeys.self)
79 | batteryLevel = try container.decodeIfPresent(Int.self, forKey: .batteryLevel) ?? -1
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/IntelliNest/Model/RoborockImageEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoborockImageEntity.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-12-11.
6 | //
7 |
8 | import Foundation
9 |
10 | struct RoborockImageEntity: EntityProtocol {
11 | let entityId: EntityId
12 | var state = ""
13 | var urlPath = ""
14 | var nextUpdate = Date.now
15 | var isActive = false
16 |
17 | init(entityId: EntityId) {
18 | self.entityId = entityId
19 | }
20 |
21 | enum CodingKeys: String, CodingKey {
22 | case entityId = "entity_id"
23 | case state
24 | case attributes
25 | }
26 |
27 | init(from decoder: Decoder) throws {
28 | let container = try decoder.container(keyedBy: CodingKeys.self)
29 | let entityId = try EntityId(rawValue: container.decode(String.self, forKey: .entityId))
30 | self.entityId = entityId ?? EntityId.unknown
31 | state = try container.decode(String.self, forKey: .state)
32 |
33 | let attributes = try container.decode(RoborockImageAttributes.self, forKey: .attributes)
34 | urlPath = attributes.entityPicture ?? "missingPath"
35 | }
36 | }
37 |
38 | struct RoborockImageAttributes: Decodable {
39 | let entityPicture: String?
40 |
41 | enum CodingKeys: String, CodingKey {
42 | case entityPicture = "entity_picture"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/IntelliNest/Model/SlideableProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SlideableProtocol.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-06-25.
6 | //
7 |
8 | import Foundation
9 |
10 | @MainActor
11 | protocol Slideable {
12 | func value(isSliding: Bool) -> Int
13 | var isOn: Bool { get }
14 | var isUpdating: Bool { get set }
15 | }
16 |
17 | extension LightEntity: Slideable {
18 | func value(isSliding: Bool) -> Int {
19 | if isOn || isSliding || isUpdating {
20 | return brightness
21 | }
22 |
23 | return 0
24 | }
25 |
26 | var isOn: Bool {
27 | isActive
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/IntelliNest/Model/SonnenStatusEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SonnenStatusEntity.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-12-20.
6 | //
7 |
8 | import Foundation
9 |
10 | enum SonnenOperationModes: String, CaseIterable, Decodable {
11 | case manual = "1"
12 | case selfConsumption = "2"
13 | case timeOfUse = "10"
14 | case unknown
15 |
16 | var title: String {
17 | switch self {
18 | case .manual:
19 | "Manual"
20 | case .selfConsumption:
21 | "Self consumption"
22 | case .timeOfUse:
23 | "Time of use"
24 | case .unknown:
25 | "Unknown"
26 | }
27 | }
28 | }
29 |
30 | struct SonnenStatusEntity: EntityProtocol {
31 | let entityId: EntityId
32 | var gridPower: Double = 0.0
33 | var operationMode = SonnenOperationModes.unknown
34 | var hasFlowGridToBattery = false
35 | var hasFlowGridToHouse = false
36 | var hasFlowSolarToHouse = false
37 | var hasFlowBatteryToHouse = false
38 | var hasFlowSolarToBattery = false
39 | var hasFlowSolarToGrid = false
40 | var state: String
41 | var nextUpdate = Date.now
42 | var isActive = false
43 |
44 | enum CodingKeys: String, CodingKey {
45 | case entityId = "entity_id"
46 | case state
47 | case attributes
48 | }
49 |
50 | init(from decoder: Decoder) throws {
51 | let container = try decoder.container(keyedBy: CodingKeys.self)
52 | entityId = try EntityId(rawValue: container.decode(String.self, forKey: .entityId)) ?? .unknown
53 | state = try container.decode(String.self, forKey: .state)
54 |
55 | let attributes = try container.decode(SonnenStatusAttributes.self, forKey: .attributes)
56 | gridPower = attributes.gridPower
57 | operationMode = attributes.operationMode
58 | hasFlowGridToBattery = attributes.hasFlowGridToBattery
59 | hasFlowGridToHouse = attributes.hasFlowGridToHouse
60 | hasFlowSolarToHouse = attributes.hasFlowSolarToHouse
61 | hasFlowBatteryToHouse = attributes.hasFlowBatteryToHouse
62 | hasFlowSolarToBattery = attributes.hasFlowSolarToBattery
63 | hasFlowSolarToGrid = attributes.hasFlowSolarToGrid
64 | }
65 | }
66 |
67 | struct SonnenStatusAttributes: Decodable {
68 | let gridPower: Double
69 | let operationMode: SonnenOperationModes
70 | let hasFlowGridToBattery: Bool
71 | let hasFlowGridToHouse: Bool
72 | let hasFlowSolarToHouse: Bool
73 | let hasFlowBatteryToHouse: Bool
74 | let hasFlowSolarToBattery: Bool
75 | let hasFlowSolarToGrid: Bool
76 |
77 | enum CodingKeys: String, CodingKey {
78 | case gridPower = "GridFeedIn_W"
79 | case operationMode = "OperatingMode"
80 | case hasFlowGridToBattery = "FlowGridBattery"
81 | case hasFlowGridToHouse = "FlowConsumptionGrid"
82 | case hasFlowSolarToHouse = "FlowConsumptionProduction"
83 | case hasFlowBatteryToHouse = "FlowConsumptionBattery"
84 | case hasFlowSolarToBattery = "FlowProductionBattery"
85 | case hasFlowSolarToGrid = "FlowProductionGrid"
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/IntelliNest/Model/SwitchEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwitchEntity.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-10-19.
6 | //
7 |
8 | import ShipBookSDK
9 | import SwiftUI
10 |
11 | struct SwitchEntity: EntityProtocol {
12 | var image: Image {
13 | isActive ? Image(systemImageName: .bolt) : Image(systemImageName: .boltSlash)
14 | }
15 |
16 | var activeColor: Color {
17 | guard isActive else {
18 | return .red
19 | }
20 |
21 | let now = Date()
22 |
23 | let elapsed = now.timeIntervalSince(lastChanged) / 60.0
24 | let minutesBeforeBlending = 1.0
25 | let minutesUntilPowered = 15.0
26 | if elapsed < minutesBeforeBlending {
27 | return .red
28 | } else if elapsed >= minutesUntilPowered {
29 | return .yellow
30 | } else {
31 | let ratio = (elapsed - minutesBeforeBlending) / (minutesUntilPowered - minutesBeforeBlending)
32 | return .blend(Color.red, with: Color.orange, ratio: CGFloat(ratio))
33 | }
34 | }
35 |
36 | var nextUpdate = Date().addingTimeInterval(-1)
37 | var isActive: Bool {
38 | state.lowercased() == "on"
39 | }
40 |
41 | var title: String {
42 | switch entityId {
43 | case .coffeeMachine:
44 | "Kaffemaskinen"
45 | default:
46 | ""
47 | }
48 | }
49 |
50 | enum CodingKeys: String, CodingKey {
51 | case entityId = "entity_id"
52 | case state
53 | case lastChanged = "last_changed"
54 | }
55 |
56 | let entityId: EntityId
57 | var state: String
58 | var lastChanged: Date
59 |
60 | init(entityId: EntityId, state: String = "Loading", lastChanged: Date = .now) {
61 | self.entityId = entityId
62 | self.state = state
63 | self.lastChanged = lastChanged
64 | }
65 |
66 | init(from decoder: Decoder) throws {
67 | let container = try decoder.container(keyedBy: CodingKeys.self)
68 | let entityId = try EntityId(rawValue: container.decode(String.self, forKey: .entityId))
69 | self.entityId = entityId ?? EntityId.unknown
70 | state = try container.decode(String.self, forKey: .state)
71 |
72 | let dateFormatter = ISO8601DateFormatter()
73 | if let lastChangedString = try? container.decode(String.self, forKey: .lastChanged),
74 | let date = dateFormatter.date(from: lastChangedString) {
75 | lastChanged = date
76 | } else {
77 | lastChanged = .distantFuture
78 | Log.error("Failed to parse last_changed in SwitchEntity")
79 | }
80 | }
81 |
82 | static func == (lhs: SwitchEntity, rhs: SwitchEntity) -> Bool {
83 | lhs.entityId == rhs.entityId
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/IntelliNest/Model/UserManager.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum User: String, CaseIterable {
4 | case sarah = "SL"
5 | case tobias = "TL"
6 | case guest = "Guest"
7 | case unknownUser = "Unknown User"
8 |
9 | var name: String {
10 | switch self {
11 | case .sarah:
12 | "Sarah"
13 | case .tobias:
14 | "Tobias"
15 | case .guest:
16 | "Gäst"
17 | case .unknownUser:
18 | "Unknown User"
19 | }
20 | }
21 | }
22 |
23 | struct UserManager {
24 | static let shared = UserManager()
25 | @MainActor
26 | static var currentUser: User {
27 | if let storedValue = UserDefaults.shared.string(forKey: StorageKeys.userInitials.rawValue),
28 | let user = User(rawValue: storedValue) {
29 | return user
30 | }
31 |
32 | return .unknownUser
33 | }
34 |
35 | @MainActor
36 | var isUserNotSet: Bool {
37 | UserDefaults.shared.string(forKey: StorageKeys.userInitials.rawValue) == nil
38 | }
39 |
40 | private init() {}
41 |
42 | func setUser(_ user: User) {
43 | Task { @MainActor in
44 | UserDefaults.shared.set(user.rawValue, forKey: StorageKeys.userInitials.rawValue)
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/IntelliNest/Model/Websocket/CallScriptRequest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CallScriptRequest.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-07-11.
6 | //
7 |
8 | import Foundation
9 |
10 | struct CallScriptRequest: Encodable {
11 | let type = "call_service"
12 | let domain = Domain.script
13 | let service = Action.turnOn
14 | var serviceData: ServiceData = EmptyServiceData()
15 | let target: Target
16 |
17 | enum CodingKeys: String, CodingKey {
18 | case type
19 | case domain
20 | case service
21 | case serviceData = "service_data"
22 | case target
23 | }
24 |
25 | struct Target: Encodable {
26 | let entityIds: [String]
27 |
28 | enum CodingKeys: String, CodingKey {
29 | case entityIds = "entity_id"
30 | }
31 | }
32 |
33 | init(scriptID: ScriptID, variables: [ScriptVariableKeys: String]? = nil) {
34 | target = Target(entityIds: [scriptID.rawValue])
35 | if let variables {
36 | serviceData = ScriptServiceData(variables: variables)
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/IntelliNest/Model/Websocket/CallServiceRequest.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import ShipBookSDK
3 |
4 | struct CallServiceRequest: Encodable {
5 | let type: String
6 | let domain: String
7 | let service: String
8 | let target: [ServiceTargetKeys: ServiceValues]?
9 | let serviceData: [ServiceDataKeys: ServiceValues]?
10 |
11 | enum CodingKeys: String, CodingKey {
12 | case type
13 | case domain
14 | case service
15 | case serviceData = "service_data"
16 | case target
17 | }
18 |
19 | init(serviceID: ServiceID,
20 | target: [ServiceTargetKeys: ServiceValues]?,
21 | serviceData: [ServiceDataKeys: ServiceValues]?) {
22 | type = "call_service"
23 |
24 | let components = serviceID.rawValue.split(separator: ".")
25 | if components.count == 2 {
26 | domain = String(components[0])
27 | service = String(components[1])
28 | } else {
29 | domain = serviceID.rawValue
30 | service = serviceID.rawValue
31 | Log.error("ServiceID \(serviceID) does not specify domain and service")
32 | }
33 |
34 | self.target = target
35 | self.serviceData = serviceData
36 | }
37 |
38 | init(serviceID: ServiceID, serviceData: [ServiceDataKeys: String]?) {
39 | self.init(serviceID: serviceID, target: nil, serviceData: serviceData?.mapValues { .string($0) })
40 | }
41 |
42 | func encode(to encoder: Encoder) throws {
43 | var container = encoder.container(keyedBy: CodingKeys.self)
44 | try container.encode(type, forKey: .type)
45 | try container.encode(domain, forKey: .domain)
46 | try container.encode(service, forKey: .service)
47 |
48 | if let target {
49 | var targetContainer = container.nestedContainer(keyedBy: ServiceTargetKeys.self, forKey: .target)
50 | for (key, value) in target {
51 | try targetContainer.encode(value, forKey: key)
52 | }
53 | }
54 |
55 | if let serviceData {
56 | var serviceDataContainer = container.nestedContainer(keyedBy: ServiceDataKeys.self, forKey: .serviceData)
57 | for (key, value) in serviceData {
58 | try serviceDataContainer.encode(value, forKey: key)
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/IntelliNest/Model/Websocket/ServiceData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServiceData.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-07-10.
6 | //
7 |
8 | import Foundation
9 | import ShipBookSDK
10 |
11 | class ServiceData: Encodable {}
12 |
13 | @MainActor
14 | class LightServiceData: ServiceData {
15 | let brightness: Int
16 | init(brightness: Int) {
17 | self.brightness = brightness
18 | }
19 |
20 | override func encode(to encoder: Encoder) throws {
21 | var container = encoder.container(keyedBy: CodingKeys.self)
22 | try container.encode(brightness, forKey: .brightness)
23 | }
24 |
25 | private enum CodingKeys: String, CodingKey {
26 | case brightness
27 | }
28 | }
29 |
30 | @MainActor
31 | class InputNumberServiceData: ServiceData {
32 | let value: Double
33 |
34 | init(value: Double) {
35 | self.value = value
36 | }
37 |
38 | override func encode(to encoder: Encoder) throws {
39 | var container = encoder.container(keyedBy: CodingKeys.self)
40 | try container.encode(value, forKey: .value)
41 | }
42 |
43 | private enum CodingKeys: String, CodingKey {
44 | case value
45 | }
46 | }
47 |
48 | @MainActor
49 | class DateTimeServiceData: ServiceData {
50 | let date: String?
51 | let time: String?
52 |
53 | init(date: Date) {
54 | let dateFormatter = DateFormatter()
55 |
56 | dateFormatter.dateFormat = "yyyy-MM-dd"
57 | self.date = dateFormatter.string(from: date)
58 |
59 | dateFormatter.dateFormat = "HH:mm:ss"
60 | time = dateFormatter.string(from: date)
61 | }
62 |
63 | override func encode(to encoder: Encoder) throws {
64 | var container = encoder.container(keyedBy: CodingKeys.self)
65 |
66 | if let date {
67 | try container.encode(date, forKey: .date)
68 | }
69 |
70 | if let time {
71 | try container.encode(time, forKey: .time)
72 | }
73 | }
74 |
75 | private enum CodingKeys: String, CodingKey {
76 | case date
77 | case time
78 | }
79 | }
80 |
81 | class ScriptServiceData: ServiceData {
82 | var variables: [ScriptVariableKeys: String]
83 |
84 | init(variables: [ScriptVariableKeys: String]) {
85 | self.variables = variables
86 | }
87 |
88 | override func encode(to encoder: any Encoder) throws {
89 | var container = encoder.container(keyedBy: CodingKeys.self)
90 | for (key, value) in variables {
91 | if let codingKey = CodingKeys(rawValue: key.rawValue) {
92 | try container.encode(value, forKey: codingKey)
93 | } else {
94 | // Log.error("ScriptServiceData failed to encode key: \(key.rawValue) with value: \(value)")
95 | }
96 | }
97 | }
98 |
99 | private enum CodingKeys: String, CodingKey {
100 | case entityID = "entity_id"
101 | }
102 | }
103 |
104 | class EmptyServiceData: ServiceData {}
105 |
--------------------------------------------------------------------------------
/IntelliNest/Model/Websocket/SubscribeRequest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SubscribeRequest.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-06-28.
6 | //
7 |
8 | import Foundation
9 |
10 | enum EventType: String, Encodable {
11 | case stateChange = "state_changed"
12 | }
13 |
14 | struct SubscribeRequest: Encodable {
15 | let type = "subscribe_events"
16 | let eventType: EventType
17 |
18 | enum CodingKeys: String, CodingKey {
19 | case type
20 | case eventType = "event_type"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/IntelliNest/Model/Websocket/UpdateEntityRequest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UpdateEntityRequest.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-06-25.
6 | //
7 |
8 | import Foundation
9 |
10 | struct UpdateEntityRequest: Encodable {
11 | let type = "call_service"
12 | let domain: Domain
13 | let service: Action
14 | var serviceData: ServiceData
15 | let target: Target
16 |
17 | enum CodingKeys: String, CodingKey {
18 | case type
19 | case domain
20 | case service
21 | case serviceData = "service_data"
22 | case target
23 | }
24 |
25 | struct Target: Encodable {
26 | let entityIds: [String]
27 |
28 | enum CodingKeys: String, CodingKey {
29 | case entityIds = "entity_id"
30 | }
31 | }
32 |
33 | init(domain: Domain, action: Action, serviceData: ServiceData? = nil, entityIds: [EntityId]) {
34 | self.domain = domain
35 | service = action
36 | self.serviceData = serviceData ?? EmptyServiceData()
37 | target = Target(entityIds: entityIds.map(\.rawValue))
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/IntelliNest/Model/YaleLock.swift:
--------------------------------------------------------------------------------
1 | //
2 | // YaleLock.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-05-18.
6 | //
7 |
8 | import Foundation
9 |
10 | struct YaleLock: Lockable, Decodable {
11 | var expectedStateSetDate: Date?
12 | let id: LockID
13 | var lockState: LockState = .unknown { didSet {
14 | if lockState == expectedState || expectedStateIsOld {
15 | expectedState = .unknown
16 | }
17 | }}
18 | var expectedState: LockState = .unknown {
19 | didSet {
20 | self.expectedStateSetDate = Date()
21 | }
22 | }
23 |
24 | var doorState: DoorState = .closed
25 | }
26 |
--------------------------------------------------------------------------------
/IntelliNest/Model/YaleLockResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // YaleLockResponse.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-05-18.
6 | //
7 |
8 | import Foundation
9 |
10 | struct LockStatus: Decodable {
11 | let status: String
12 | let dateTime: String
13 | let isLockStatusChanged: Bool
14 | let valid: Bool
15 | let doorState: DoorState
16 | }
17 |
18 | struct YaleLockResponse: Decodable {
19 | let lockStatus: LockStatus
20 |
21 | enum CodingKeys: String, CodingKey {
22 | case lockStatus = "LockStatus"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/IntelliNest/Navigation/Destination.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Destination.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-04-30.
6 | //
7 |
8 | import Foundation
9 |
10 | enum Destination: String {
11 | case electricity
12 | case home
13 | case heaters
14 | case lynk
15 | case corridorHeaterDetails
16 | case playroomHeaterDetails
17 | case roborock
18 | case lights
19 |
20 | var title: String {
21 | switch self {
22 | case .electricity:
23 | "Ström"
24 | case .home:
25 | "Hem"
26 | case .heaters:
27 | "Värmepumpar"
28 | case .lynk:
29 | "Lynk"
30 | case .corridorHeaterDetails:
31 | "Korridoren"
32 | case .playroomHeaterDetails:
33 | "Lekrummet"
34 | case .roborock:
35 | "Roborock"
36 | case .lights:
37 | "Lampor"
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/IntelliNest/Navigation/NavigatorHelpers.swif:
--------------------------------------------------------------------------------
1 | import Foundation
2 | extension Navigator
3 |
--------------------------------------------------------------------------------
/IntelliNest/Navigation/NavigatorHelpers.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension Navigator {
4 | func pop() {
5 | navigationPath.removeLast()
6 | }
7 |
8 | func show(destination: Destination) -> some View {
9 | Group {
10 | switch destination {
11 | case .electricity:
12 | showElectricityView()
13 | case .home:
14 | Text("Not implemented show for home")
15 | case .heaters:
16 | showHeatersView()
17 | case .lynk:
18 | showLynkView()
19 | case .corridorHeaterDetails:
20 | showHeaterDetailsView(heaterID: .heaterCorridor)
21 | case .playroomHeaterDetails:
22 | showHeaterDetailsView(heaterID: .heaterPlayroom)
23 | case .roborock:
24 | showRoborockView()
25 | case .lights:
26 | showLightsView()
27 | }
28 | }
29 | .backgroundModifier()
30 | }
31 |
32 | func push(_ destination: Destination) {
33 | if currentDestination != destination {
34 | Task {
35 | if destination == .home {
36 | navigationPath = []
37 | } else {
38 | navigationPath.append(destination)
39 | }
40 |
41 | await reloadCurrentModel()
42 | }
43 | }
44 | }
45 |
46 | func showElectricityView() -> ElectricityView {
47 | ElectricityView(viewModel: electricityViewModel)
48 | }
49 |
50 | func showHomeView() -> HomeView {
51 | HomeView(viewModel: homeViewModel)
52 | }
53 |
54 | func showHeatersView() -> HeatersView {
55 | HeatersView(viewModel: heatersViewModel)
56 | }
57 |
58 | func showLynkView() -> LynkView {
59 | LynkView(viewModel: lynkViewModel)
60 | }
61 |
62 | func showHeaterDetailsView(heaterID: EntityId) -> DetailedHeaterView {
63 | if heaterID == .heaterCorridor {
64 | DetailedHeaterView(viewModel: heatersViewModel, selectedHeater: .corridor)
65 | } else {
66 | DetailedHeaterView(viewModel: heatersViewModel, selectedHeater: .playroom)
67 | }
68 | }
69 |
70 | func showRoborockView() -> RoborockView {
71 | RoborockView(viewModel: roborockViewModel)
72 | }
73 |
74 | func showLightsView() -> LightsView {
75 | LightsView(viewModel: lightsViewModel)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/IntelliNest/Navigation/ToolbarBackButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ToolbarBackButton.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-05-02.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ToolbarBackButton: View {
11 | @Environment(\.dismiss) private var dismiss
12 |
13 | var body: some View {
14 | Button {
15 | dismiss()
16 | } label: {
17 | Image(systemName: "chevron.left")
18 | .foregroundColor(.white)
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/IntelliNest/Navigation/ToolbarItems.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ToolbarItems.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-05-02.
6 | //
7 |
8 | import ShipBookSDK
9 | import SwiftUI
10 |
11 | struct ToolBarConnectionStateView: View {
12 | @ObservedObject var urlCreator: URLCreator
13 |
14 | var body: some View {
15 | Button {
16 | Task { @MainActor in
17 | await urlCreator.updateConnectionState()
18 | }
19 | } label: {
20 | Group {
21 | switch urlCreator.connectionState {
22 | case .local:
23 | Image(systemName: "wifi")
24 | case .internet:
25 | Image(systemName: "globe")
26 | case .disconnected:
27 | Image(systemName: "wifi.exclamationmark")
28 | case .loading:
29 | ProgressView()
30 | .progressViewStyle(CircularProgressViewStyle())
31 | case .unset:
32 | Text("?")
33 | }
34 | }
35 | .foregroundColor(.white)
36 | .padding(.trailing)
37 | }
38 | }
39 | }
40 |
41 | struct ToolbarTitleView: View {
42 | var destination: Destination
43 |
44 | var body: some View {
45 | Text(destination.title)
46 | .foregroundColor(.white)
47 | .font(.headline)
48 | }
49 | }
50 |
51 | struct ToolbarReloadButtonView: View {
52 | var destination: Destination
53 | var navigator: Navigator?
54 | var reloadAction: MainActorAsyncVoidClosure?
55 | @State var isLoading = false
56 |
57 | var body: some View {
58 | Button {
59 | Task { @MainActor in
60 | isLoading = true
61 | if let reloadAction {
62 | await reloadAction()
63 | } else if let navigator {
64 | await navigator.reloadConnection()
65 | await navigator.reload(for: destination)
66 | } else {
67 | Log.error("Missing reload action in ToolbarReloadButton for \(destination)")
68 | }
69 | isLoading = false
70 | }
71 | } label: {
72 | Image(imageName: .refresh)
73 | .foregroundColor(.white)
74 | .rotationEffect(.degrees(isLoading ? 360 : 0))
75 | .animation(isLoading ? .linear(duration: 1) : nil, value: isLoading)
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/IntelliNest/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/IntelliNest/Services/GeofenceManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GeofenceManager.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2024-01-22.
6 | //
7 |
8 | import CoreLocation
9 | import Foundation
10 | import ShipBookSDK
11 | import UserNotifications
12 |
13 | class GeofenceManager: NSObject {
14 | private let locationManager = CLLocationManager()
15 | private let didEnterHomeAction: VoidClosure
16 | private let didExitHomeAction: VoidClosure
17 |
18 | init(didEnterHomeAction: @escaping VoidClosure, didExitHomeAction: @escaping VoidClosure) {
19 | self.didEnterHomeAction = didEnterHomeAction
20 | self.didExitHomeAction = didExitHomeAction
21 | super.init()
22 | locationManager.delegate = self
23 | locationManager.desiredAccuracy = kCLLocationAccuracyBest
24 | locationManager.requestAlwaysAuthorization()
25 | }
26 |
27 | func configureGeoFence(homeCoordinates: Coordinates) {
28 | for region in locationManager.monitoredRegions {
29 | if let circularRegion = region as? CLCircularRegion {
30 | locationManager.stopMonitoring(for: circularRegion)
31 | }
32 | }
33 |
34 | let geofenceRegion = CLCircularRegion(center: homeCoordinates.toCLLocationCoordinate2D(),
35 | radius: 50,
36 | identifier: "HomeRegion")
37 | geofenceRegion.notifyOnExit = true
38 | geofenceRegion.notifyOnEntry = true
39 |
40 | startMonitoring(geofenceRegion: geofenceRegion)
41 | }
42 |
43 | private func startMonitoring(geofenceRegion: CLCircularRegion) {
44 | if CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) {
45 | locationManager.startMonitoring(for: geofenceRegion)
46 | }
47 | }
48 | }
49 |
50 | extension GeofenceManager: CLLocationManagerDelegate {
51 | func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
52 | if region is CLCircularRegion {
53 | if !UserDefaults.standard.bool(forKey: StorageKeys.isHome.rawValue) {
54 | UserDefaults.standard.set(true, forKey: StorageKeys.isHome.rawValue)
55 | didEnterHomeAction()
56 | }
57 | }
58 | }
59 |
60 | func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
61 | if region is CLCircularRegion {
62 | if UserDefaults.standard.bool(forKey: StorageKeys.isHome.rawValue) {
63 | UserDefaults.standard.set(false, forKey: StorageKeys.isHome.rawValue)
64 | didExitHomeAction()
65 | }
66 | }
67 | }
68 |
69 | func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
70 | Log.error("Location manager error: \(error)")
71 | }
72 |
73 | func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) {
74 | Log.error("Location manager failure: \(error)")
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/IntelliNest/Services/NetworkTask.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkTask.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-05-25.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol NetworkTask {
11 | func resume()
12 | }
13 |
14 | extension URLSessionDataTask: NetworkTask {}
15 |
--------------------------------------------------------------------------------
/IntelliNest/Services/NotificationService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotificationService.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2024-01-25.
6 | //
7 |
8 | import Foundation
9 | import ShipBookSDK
10 | import UserNotifications
11 |
12 | class NotificationService {
13 | init() {}
14 |
15 | static func sendNotification(title: String, message: String, identifier: String) {
16 | let content = UNMutableNotificationContent()
17 | content.title = title
18 | content.body = message
19 | content.sound = UNNotificationSound.default
20 |
21 | let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
22 | UNUserNotificationCenter.current().add(request) { error in
23 | if let error {
24 | Log.error("Error scheduling notification: \(error)")
25 | }
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/IntelliNest/Services/PreviewProvideUtil.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PreviewProvideUtil.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2024-01-25.
6 | //
7 |
8 | import Foundation
9 |
10 | @MainActor
11 | class PreviewProviderUtil {
12 | static var urlCreator = URLCreator()
13 | static var restAPIService = RestAPIService(urlCreator: PreviewProviderUtil.urlCreator, setErrorBannerText: { _, _ in },
14 | repeatReloadAction: { _ in })
15 | static var electricityViewModel = ElectricityViewModel(sonnenBattery: .init(entityID: .sonnenBattery),
16 | restAPIService: PreviewProviderUtil.restAPIService)
17 | static var heatersViewModel = HeatersViewModel(restAPIService: restAPIService, showHeaterDetails: { _ in })
18 | static var lynkViewModel = LynkViewModel(restAPIService: restAPIService)
19 |
20 | private init() {}
21 | }
22 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/AppIntentVocabulary.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IntentPhrases
6 |
7 |
8 | IntentName
9 | INSendMessageIntent
10 | IntentExamples
11 |
12 | Text example
13 |
14 |
15 |
16 | IntentName
17 | INSetMessageAttributeIntent
18 | IntentExamples
19 |
20 | Text example attribute
21 |
22 |
23 |
24 | IntentName
25 | INSearchForMessagesIntent
26 | IntentExamples
27 |
28 | Example message
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/App Store.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/App Store.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad App Icon iOS 7,12 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad App Icon iOS 7,12 1.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad App Icon iOS 7,12@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad App Icon iOS 7,12@2x.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad Pro Icon iOS 9,12@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad Pro Icon iOS 9,12@2x.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad Settings 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad Settings 1.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad Settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad Settings.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad Settings@2x 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad Settings@2x 1.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad Settings@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad Settings@2x.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad Spotlight iOS 7,12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad Spotlight iOS 7,12.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad Spotlight iOS 7,12@2x 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad Spotlight iOS 7,12@2x 1.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad Spotlight iOS 7,12@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad Spotlight iOS 7,12@2x.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad notification iOS 7,12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad notification iOS 7,12.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad notification iOS 7,12@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPad notification iOS 7,12@2x.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPhone App Icon iOS 5,6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPhone App Icon iOS 5,6.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPhone App Icon iOS 5,6@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPhone App Icon iOS 5,6@2x.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPhone App Icon iOS 7,12@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPhone App Icon iOS 7,12@2x.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPhone App Icon iOS 7,12@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPhone App Icon iOS 7,12@3x.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPhone Spotlight iOS 5,6 & Settings@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPhone Spotlight iOS 5,6 & Settings@3x.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPhone Spotlight iOS 7,12@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPhone Spotlight iOS 7,12@3x.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPhone notification iOS 7,12@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPhone notification iOS 7,12@2x.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPhone notification iOS 7,12@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/AppIcon.appiconset/iPhone notification iOS 7,12@3x.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/Images.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x"
10 | },
11 | {
12 | "idiom" : "universal",
13 | "scale" : "3x"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/aircondition.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "heater.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/aircondition.imageset/heater.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/aircondition.imageset/heater.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/defrost.filled.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "defrost.filled.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | },
21 | "properties" : {
22 | "preserves-vector-representation" : true,
23 | "template-rendering-intent" : "template"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/defrost.filled.imageset/defrost.filled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/defrost.filled.imageset/defrost.filled.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/ev-plug-ccs2.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "ev-plug-ccs2.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/ev-plug-ccs2.imageset/ev-plug-ccs2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/ev-plug-ccs2.imageset/ev-plug-ccs2.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/ev-plug-type2.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "ev-plug-type2.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/ev-plug-type2.imageset/ev-plug-type2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/ev-plug-type2.imageset/ev-plug-type2.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/floorplan.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "floorplan.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/floorplan.imageset/floorplan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/floorplan.imageset/floorplan.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/gym.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "gym.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/gym.imageset/gym.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/gym.imageset/gym.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/hallway.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "hallway.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/hallway.imageset/hallway.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/hallway.imageset/hallway.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/powergrid.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "powergrid.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/powergrid.imageset/powergrid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/powergrid.imageset/powergrid.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/refresh.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "icons8-iOS Glyph-tovbiOioOGAO-50-ffffff.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/refresh.imageset/icons8-iOS Glyph-tovbiOioOGAO-50-ffffff.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/refresh.imageset/icons8-iOS Glyph-tovbiOioOGAO-50-ffffff.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/roborocks7.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon_128x128.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/roborocks7.imageset/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/roborocks7.imageset/icon_128x128.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/seatheater.filled.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "seatheater.filled.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | },
21 | "properties" : {
22 | "preserves-vector-representation" : true,
23 | "template-rendering-intent" : "template"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/seatheater.filled.imageset/seatheater.filled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/seatheater.filled.imageset/seatheater.filled.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/settings.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "settings.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/settings.imageset/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/settings.imageset/settings.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/solarpanel.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "solarpanel.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/solarpanel.imageset/solarpanel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/solarpanel.imageset/solarpanel.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/vince.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "vince.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/vince.imageset/vince.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/vince.imageset/vince.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/washing.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "washing.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Assets.xcassets/washing.imageset/washing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliNest/Supporting Files/Assets.xcassets/washing.imageset/washing.png
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleURLTypes
6 |
7 |
8 | CFBundleURLName
9 | se.laross.intellinest
10 | CFBundleURLSchemes
11 |
12 | IntelliNest
13 |
14 |
15 |
16 | UIBackgroundModes
17 |
18 | fetch
19 |
20 | EXTERNAL_URL
21 | $(EXTERNAL_URL)
22 | ITSAppUsesNonExemptEncryption
23 |
24 | LOCAL_SSID
25 | $(LOCAL_SSID)
26 | NSUserActivityTypes
27 |
28 | SECRET_HASS_TOKEN
29 | $(SECRET_HASS_TOKEN)
30 | SECRET_HASS_TOKEN_SARAH
31 | $(SECRET_HASS_TOKEN_SARAH)
32 | SECRET_RTSP_STREAM_CAMERA_BACK
33 | $(SECRET_RTSP_STREAM_CAMERA_BACK)
34 | SECRET_RTSP_STREAM_CAMERA_CARPORT
35 | $(SECRET_RTSP_STREAM_CAMERA_CARPORT)
36 | SECRET_RTSP_STREAM_CAMERA_FRONT
37 | $(SECRET_RTSP_STREAM_CAMERA_FRONT)
38 | SECRET_RTSP_STREAM_CAMERA_VINCE
39 | $(SECRET_RTSP_STREAM_CAMERA_VINCE)
40 | SECRET_SHIP_BOOK_APP_ID
41 | $(SECRET_SHIP_BOOK_APP_ID)
42 | SECRET_SHIP_BOOK_APP_KEY
43 | $(SECRET_SHIP_BOOK_APP_KEY)
44 | SECRET_YALE_API_KEY
45 | $(SECRET_YALE_API_KEY)
46 | SECRET_YALE_API_URL
47 | $(SECRET_YALE_API_URL)
48 | UIApplicationSceneManifest
49 |
50 | UIApplicationSupportsMultipleScenes
51 |
52 | UISceneConfigurations
53 |
54 |
55 | UIApplicationShortcutItems
56 |
57 |
58 | UIApplicationShortcutItemIconSymbolName
59 | thermometer
60 | UIApplicationShortcutItemTitle
61 | Starta bilens klimat
62 | UIApplicationShortcutItemType
63 | CarHeater
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/IntelliNest/Supporting Files/IntelliNest.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | aps-environment
6 | development
7 | com.apple.developer.networking.HotspotConfiguration
8 |
9 | com.apple.developer.networking.wifi-info
10 |
11 | com.apple.security.application-groups
12 |
13 | group.se.laross.intellinest.shared
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Constants/Actions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Actions.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-08-23.
6 | //
7 |
8 | import Foundation
9 |
10 | enum Action: String, Codable {
11 | case kiaUpdate = "update"
12 | case kiaForceUpdate = "force_update"
13 | case kiaStopCharge = "stop_charge"
14 | case kiaStartCharge = "start_charge"
15 | case kiaStartClimate = "start_climate"
16 | case stopClimate = "stop_climate"
17 | case kiaLimitCharger = "set_charge_limits"
18 | case lock
19 | case unlock
20 | case turnOn = "turn_on"
21 | case turnOff = "turn_off"
22 | case locate
23 | case register
24 | case snapshot
25 | case stop
26 | case start
27 | case lynkReload = "force_update_data"
28 | case lockDoors = "lock_doors"
29 | case unlockDoors = "unlock_doors"
30 | case startFlashLights = "start_flash_lights"
31 | case stopFlashLights = "stop_flash_lights"
32 | case setDateTime = "set_datetime"
33 | case setValue = "set_value"
34 | case setVaneHorizontal = "set_vane_horizontal"
35 | case setVaneVertical = "set_vane_vertical"
36 | case setFanMode = "set_fan_mode"
37 | case setPresetMode = "set_preset_mode"
38 | case startEngine = "start_engine"
39 | case stopEngine = "stop_engine"
40 | case setTemperature = "set_temperature"
41 | case setPercentage = "set_percentage"
42 | case setHvacMode = "set_hvac_mode"
43 | case updateEntity = "update_entity"
44 | case sonnenOperationMode = "sonnen_put_config_operation_mode"
45 | case sonnenCharge = "sonnen_charge"
46 | case sonnenDischarge = "sonnen_discharge"
47 | }
48 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Constants/Closures.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Closures.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-10-07.
6 | //
7 |
8 | import Foundation
9 |
10 | typealias VoidClosure = () -> Void
11 | typealias MainActorVoidClosure = @MainActor () -> Void
12 | typealias MainActorEntityIDClosure = @MainActor (EntityId) -> Void
13 | typealias AsyncVoidClosure = () async -> Void
14 | typealias MainActorAsyncVoidClosure = @MainActor () async -> Void
15 | typealias StringClosure = (String) -> Void
16 | typealias StringStringClosure = (String, String) -> Void
17 | typealias DoubleClosure = (Double) -> Void
18 | typealias IntClosure = (Int) -> Void
19 | typealias HeaterDoubleClosure = @MainActor (HeaterEntity, Double) -> Void
20 | typealias HeaterStringClosure = @MainActor (HeaterEntity, HvacMode) -> Void
21 | typealias HeaterFanModeClosure = @MainActor (HeaterEntity, HeaterFanMode) -> Void
22 | typealias FanModeClosure = (HeaterFanMode) -> Void
23 | typealias HeaterHorizontalModeClosure = @MainActor (HeaterEntity, HeaterHorizontalMode) -> Void
24 | typealias HorizontalModeClosure = (HeaterHorizontalMode) -> Void
25 | typealias HeaterVerticalModeClosure = @MainActor (HeaterEntity, HeaterVerticalMode) -> Void
26 | typealias VerticalModeClosure = (HeaterVerticalMode) -> Void
27 | typealias EntityClosure = (Entity) -> Void
28 | typealias MainActorEntityClosure = @MainActor (Entity) -> Void
29 | typealias AsyncEntityClosure = (Entity) async -> Void
30 | typealias ScriptIDClosure = (ScriptID) -> Void
31 | typealias EntityIdDoubleClosure = @MainActor (EntityId, Double) -> Void
32 | typealias AsyncLightClosure = (LightEntity) async -> Void
33 | typealias AsyncSlideableClosure = @MainActor (Slideable) async -> Void
34 | typealias SlideableIntClosure = @MainActor (Slideable, Int) -> Void
35 | typealias AsyncSlideableIntClosure = (Slideable, Int) async -> Void
36 | typealias DestinationClosure = (Destination) -> Void
37 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Constants/DeviceID.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DeviceID.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-02-03.
6 | //
7 |
8 | import Foundation
9 |
10 | enum DeviceID: String, Decodable {
11 | case eniro = "e6bf834649633a87c6c50dd51dbe9421"
12 | }
13 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Constants/Domain.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Domain.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-08-23.
6 | //
7 |
8 | import Foundation
9 |
10 | enum Domain: String, Encodable {
11 | case apnsToken
12 | case camera
13 | case climate
14 | case fan
15 | case melcloud
16 | case switchDomain = "switch"
17 | case lock
18 | case script
19 | case automation
20 | case homeassistant
21 | case restCommand = "rest_command"
22 | case leaf = "nissan_carwings"
23 | case light
24 | case lynkco
25 | case inputBoolean = "input_boolean"
26 | case inputDateTime = "input_datetime"
27 | case inputNumber = "input_number"
28 | case sensor
29 | case binarySensor = "binary_sensor"
30 | case kiaUvo = "kia_uvo"
31 | case vacuum
32 | case unknown
33 | }
34 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Constants/FullScreenBackgroundOverlay.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FullScreenBackgroundOverlay.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-02-22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct FullScreenBackgroundOverlay: View {
11 | var body: some View {
12 | Rectangle()
13 | .foregroundStyle(Color.bodyColor)
14 | .opacity(0.5)
15 | .edgesIgnoringSafeArea(.all)
16 | }
17 | }
18 |
19 | struct FullScreenBackgroundOverlay_Previews: PreviewProvider {
20 | static var previews: some View {
21 | FullScreenBackgroundOverlay()
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Constants/HTTPMethod.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPMethod.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-05-18.
6 | //
7 |
8 | import Foundation
9 |
10 | enum HTTPMethod: String, Equatable {
11 | case get
12 | case post
13 | case put
14 | case delete
15 | }
16 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Constants/Heater.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Heater.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-08-23.
6 | //
7 |
8 | import Foundation
9 |
10 | enum HvacMode: String {
11 | case off
12 | case heat
13 | case cool
14 | }
15 |
16 | enum HeaterFanMode: String, Decodable {
17 | case auto
18 | case one = "1"
19 | case two = "2"
20 | case three = "3"
21 | case four = "4"
22 | case five = "5"
23 | case unknown
24 | }
25 |
26 | enum HeaterHorizontalMode: String, Encodable {
27 | case auto
28 | case oneLeft = "1_left"
29 | case two = "2"
30 | case three = "3"
31 | case four = "4"
32 | case fiveRight = "5_right"
33 | case swing
34 | case split
35 | case unknown
36 | }
37 |
38 | enum HeaterVerticalMode: String, Encodable {
39 | case auto
40 | case highest = "1_up"
41 | case position2 = "2"
42 | case position3 = "3"
43 | case position4 = "4"
44 | case lowest = "5_down"
45 | case swing
46 | case unknown
47 | }
48 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Constants/JSONKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONKey.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-05-18.
6 | //
7 |
8 | import Foundation
9 |
10 | enum JSONKey: String, Equatable, Codable, Hashable {
11 | case invalid
12 | case appData = "app_data"
13 | case appID = "app_id"
14 | case appName = "app_name"
15 | case appVersion = "app_version"
16 | case data
17 | case entityID = "entity_id"
18 | case deviceID = "device_id"
19 | case deviceName = "device_name"
20 | case brightness
21 | case dateTime = "datetime"
22 | case manufacturer
23 | case model
24 | case osName = "os_name"
25 | case osVersion = "os_version"
26 | case time
27 | case percentage
28 | case presetMode = "preset_mode"
29 | case temperature
30 | case hvacMode = "hvac_mode"
31 | case fanMode = "fan_mode"
32 | case position
33 | case duration
34 | case climate
35 | case defrost
36 | case heating
37 | case flseat
38 | case frseat
39 | case filename
40 | case variables
41 | case acLimit = "ac_limit"
42 | case dcLimit = "dc_limit"
43 | case supportsEncryption = "supports_encryption"
44 | case pushToken = "push_token"
45 | case pushURL = "push_url"
46 | case operationMode
47 | case yaleAccessTokenFull = "yale_access_token_full"
48 | case value
49 | case vin
50 | case watt
51 | }
52 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Constants/LockID.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LockID.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-05-18.
6 | //
7 |
8 | import Foundation
9 |
10 | enum LockID: String, Decodable {
11 | case sideDoor = "7A274D712EF3B541A584EC739A0502A2"
12 | case frontDoor = "57034E453D405F4C8F9085C19EDF14D1"
13 | case storageDoor = "storage"
14 | case lynkDoor = "lynk"
15 | }
16 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Constants/ScriptID.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScriptID.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-07-11.
6 | //
7 |
8 | import Foundation
9 |
10 | enum ScriptID: String, Decodable, CaseIterable {
11 | case roborockDock = "script.docka_bob"
12 | case roborockSendToBin = "script.bob_send_to_bin"
13 | case roborockManualEmpty = "script.roborock_manual_empty"
14 | case roborockKitchen = "script.dammsug_koket"
15 | case roborockLaundry = "script.dammsug_tvattstugan"
16 | case roborockCorridor = "script.dammsug_korridoren"
17 | case roborockHallway = "script.dammsug_hallen"
18 | case roborockBedroom = "script.dammsug_sovrummet"
19 | case roborockGym = "script.dammsug_gymmet"
20 | case roborockLivingroom = "script.dammsug_vardagsrummet"
21 | case roborockVinceRoom = "script.dammsug_vince_rum"
22 | case roborockKitchenTable = "script.dammsug_matbord"
23 | case roborockKitchenStove = "script.dammsug_matlagning"
24 | case saveClimateState = "script.save_climate_state"
25 | case eniroStartClimate = "script.kia_climate_control"
26 | case eniroTurnOffStartClimate = "script.turn_off_kia_climate_control"
27 | case lynkStartClimate = "script.lynk_start_climate"
28 | case lynkStopClimate = "script.lynk_stop_climate"
29 | case lynkStartEngine = "script.lynk_start_engine"
30 | case lynkStopEngine = "script.lynk_stop_engine"
31 | case easeeToggle = "script.toggle_easee"
32 | }
33 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Constants/ScriptVariableKeys.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScriptVariableKeys.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-06-16.
6 | //
7 |
8 | import Foundation
9 |
10 | enum ScriptVariableKeys: String, Equatable, Codable, Hashable {
11 | case entityID = "entity_id"
12 | }
13 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Constants/ServiceDataKeys.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServiceDataKeys.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-11-17.
6 | //
7 |
8 | import Foundation
9 |
10 | enum ServiceDataKeys: String, Equatable, Codable, Hashable, CodingKey {
11 | case service
12 | case entityID = "entity_id"
13 | case deviceID = "device_id"
14 | case dcLimit = "dc_limit"
15 | case acLimit = "ac_limit"
16 | case temperature
17 | case hvacMode = "hvac_mode"
18 | case fanMode = "fan_mode"
19 | case position
20 | case date
21 | case time
22 | case datetime
23 | case watt
24 | case operationMode
25 | }
26 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Constants/ServiceID.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServiceID.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-11-17.
6 | //
7 |
8 | import Foundation
9 | import ShipBookSDK
10 |
11 | enum ServiceID: String, Decodable, CaseIterable {
12 | case kiaForceUpdate = "kia_uvo.force_update"
13 | case kiaUpdate = "kia_uvo.update"
14 | case kiaLock = "kia_uvo.lock"
15 | case kiaUnlock = "kia_uvo.unlock"
16 | case kiaStartCharge = "kia_uvo.start_charge"
17 | case kiaStopCharge = "kia_uvo.stop_charge"
18 | case kiaChargeLimit = "kia_uvo.set_charge_limits"
19 | case kiaStopClimate = "kia_uvo.stop_climate"
20 | case leafUpdate = "nissan_carwings.update"
21 | case leafStartClimate = "nissan_carwings.start_climate"
22 | case leafStopClimate = "nissan_carwings.stop_climate"
23 | case leafStartCharging = "nissan_carwings.start_charging"
24 | case lynkReload = "lynkco.force_update_data"
25 | case lynkLockDoors = "lynkco.lock_doors"
26 | case lynkUnlockDoors = "lynkco.unlock_doors"
27 | case lynkFlashStart = "lynkco.start_flash_lights"
28 | case lynkFlashStop = "lynkco.stop_flash_lights"
29 | case lynkStartEngine = "lynkco.start_engine"
30 | case lynkStopEngine = "lynkco.stop_engine"
31 | case cameraStream = "camera/stream"
32 | case automationTurnOn = "automation.turn_on"
33 | case automationTurnOff = "automation.turn_off"
34 | case boolTurnOn = "input_boolean.turn_on"
35 | case boolTurnOff = "input_boolean.turn_off"
36 | case updateEntity = "homeassistant.update_entity"
37 | case heaterTemperature = "climate.set_temperature"
38 | case heaterHvacMode = "climate.set_hvac_mode"
39 | case heaterFanMode = "climate.set_fan_mode"
40 | case heaterHorizontal = "melcloud.set_vane_horizontal"
41 | case heaterVertical = "melcloud.set_vane_vertical"
42 | case setDateTime = "input_datetime.set_datetime"
43 | case sonnenOperationMode = "rest_command.sonnen_put_config_operation_mode"
44 | case sonnenCharge = "rest_command.sonnen_charge"
45 | case sonnenDischarge = "rest_command.sonnen_discharge"
46 |
47 | @MainActor
48 | var toAction: Action? {
49 | if let action = Action(rawValue: rawValue.components(separatedBy: ".").last ?? "") {
50 | return action
51 | } else {
52 | Log.error("Failed to create action from ServiceID: \(rawValue)")
53 | return nil
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Constants/ServiceTargetKeys.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServiceTargetKeys.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2024-01-11.
6 | //
7 |
8 | import Foundation
9 |
10 | enum ServiceTargetKeys: String, CodingKey {
11 | case entityID = "entity_id"
12 | }
13 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Constants/ServiceValues.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServiceValues.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-11-17.
6 | //
7 |
8 | import Foundation
9 |
10 | enum ServiceValues: Encodable {
11 | case string(String)
12 | case int(Int)
13 | case double(Double)
14 | case stringArray([String])
15 |
16 | func encode(to encoder: Encoder) throws {
17 | var container = encoder.singleValueContainer()
18 | switch self {
19 | case let .string(stringValue):
20 | try container.encode(stringValue)
21 | case let .int(intValue):
22 | try container.encode(intValue)
23 | case let .double(doubleValue):
24 | try container.encode(doubleValue)
25 | case let .stringArray(stringArrayValue):
26 | try container.encode(stringArrayValue)
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Constants/StorageKeys.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StorageKeys.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-05-18.
6 | //
7 |
8 | import Foundation
9 |
10 | enum StorageKeys: String {
11 | case homeCoordinatesDual
12 | case enteredHomeTime
13 | case isHome
14 | case lynkReloadTime
15 | case leafReloadTime
16 | case sarahPills
17 | case userInitials
18 | case webhookID
19 | case yaleAccessToken
20 | }
21 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Extensions/CalendarExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CalendarExtension.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-02-08.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Calendar {
11 | static var currentHour: Int {
12 | Calendar.current.component(.hour, from: Date())
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Extensions/CollectionExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CollectionExtension.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-10-22.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Collection {
11 | var isNotEmpty: Bool {
12 | !isEmpty
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Extensions/DateExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DateExtension.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-03-05.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Date {
11 | static func fromISO8601(_ dateString: String?) -> Date? {
12 | guard let dateString else {
13 | return nil
14 | }
15 |
16 | let dateFormatter = DateFormatter()
17 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ"
18 | return dateFormatter.date(from: dateString)
19 | }
20 |
21 | func daysRemainingDescription() -> String? {
22 | let currentDate = Calendar.current.startOfDay(for: Date())
23 | let calendar = Calendar.current
24 | let components = calendar.dateComponents([.day], from: currentDate, to: self)
25 |
26 | if let daysRemaining = components.day {
27 | switch daysRemaining {
28 | case 0:
29 | return "idag"
30 | case 1:
31 | return "imorgon"
32 | default:
33 | break
34 | }
35 | }
36 |
37 | return nil
38 | }
39 |
40 | var humanReadable: String {
41 | let now = Date()
42 | let calendar = Calendar.current
43 |
44 | if let minutesAgo = calendar.dateComponents([.minute], from: self, to: now).minute, minutesAgo < 60 {
45 | if minutesAgo == 0 {
46 | return "Just now"
47 | } else if minutesAgo == 1 {
48 | return "\(minutesAgo) minute ago"
49 | } else {
50 | return "\(minutesAgo) minutes ago"
51 | }
52 | }
53 |
54 | if let hoursAgo = calendar.dateComponents([.hour], from: self, to: now).hour, hoursAgo < 24 {
55 | return "\(hoursAgo) \(hoursAgo == 1 ? "hour" : "hours") ago"
56 | }
57 |
58 | if calendar.isDateInYesterday(self) {
59 | return "Yesterday"
60 | }
61 |
62 | let formatter = DateFormatter()
63 | formatter.dateStyle = .medium
64 | formatter.timeStyle = .short
65 | formatter.locale = Locale.current
66 | return formatter.string(from: self)
67 | }
68 |
69 | func minutesLeft() -> Int {
70 | let timeZoneOffset = TimeZone.current.secondsFromGMT()
71 | let selfInLocalTimezone = addingTimeInterval(TimeInterval(timeZoneOffset))
72 | let now = Date().addingTimeInterval(TimeInterval(timeZoneOffset))
73 | return Calendar.current.dateComponents([.minute], from: now, to: selfInLocalTimezone).minute ?? -1
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Extensions/DoubleExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DoubleExtension.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-11-23.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Double {
11 | var roundedWithOneDecimal: Double {
12 | var temp = self * 10
13 | temp.round()
14 | return temp / 10
15 | }
16 |
17 | var toPercent: String {
18 | let roundedPercent = roundedWithOneDecimal
19 | return roundedPercent < 0.06 ? "0%" : "\(String(format: "%.1f", roundedPercent))%"
20 | }
21 |
22 | var toKW: Double {
23 | let kiloWatt = self / 1000.0
24 | return (abs(kiloWatt) < 0.06 ? 0 : kiloWatt).roundedWithOneDecimal
25 | }
26 |
27 | var toKWString: String {
28 | let kiloWatt = self / 1000.0
29 | let roundedKilowWatt = (abs(kiloWatt) < 0.06 ? 0 : kiloWatt).roundedWithOneDecimal
30 | return roundedKilowWatt == 0 ? "\(Int(roundedKilowWatt))kW" : String(format: "%.1fkW", roundedKilowWatt)
31 | }
32 |
33 | var toFanSpeedPercentage: Double {
34 | if self == 0 {
35 | 0
36 | } else if self == 1 {
37 | 11
38 | } else {
39 | (self + 2) * 10
40 | }
41 | }
42 |
43 | var toFanSpeedTargetNumber: Double {
44 | switch self {
45 | case 11:
46 | 1
47 | case 33:
48 | 2
49 | case 44:
50 | 3
51 | case 55:
52 | 4
53 | case 66:
54 | 5
55 | case 77:
56 | 6
57 | case 88:
58 | 7
59 | case 100:
60 | 8
61 | default:
62 | 0
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Extensions/EncodableExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EncodableExtension.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-06-25.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Encodable {
11 | var dictionary: [String: Any]? {
12 | guard let data = try? JSONEncoder().encode(self) else { return nil }
13 | return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Extensions/FontExtension.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension Font {
4 | static var buttonFontExtraSmall: Font {
5 | .system(size: 8)
6 | }
7 |
8 | static var buttonFontSmall: Font {
9 | .system(size: 10)
10 | }
11 |
12 | static var buttonFontMedium: Font {
13 | .system(size: 12)
14 | }
15 |
16 | static var buttonFontLarge: Font {
17 | .system(size: 14)
18 | }
19 |
20 | static var buttonFontExtraLarge: Font {
21 | .system(size: 24)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Extensions/HomeViewModelExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeViewModelExtension.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-11-23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension HomeViewModel {
11 | var sarahIphoneimage: Image {
12 | if sarahsIphone.isActive {
13 | Image(systemImageName: .iPhoneActive)
14 | } else {
15 | Image(systemImageName: .iPhone)
16 | }
17 | }
18 |
19 | var dynamicInfoText: String {
20 | var text = ""
21 | let washerCompletionInMinutes = washerCompletionTime.date.minutesLeft()
22 | if washerCompletionInMinutes >= 0 && washerState.state.isRunning() {
23 | text.addNewLineAndAppend("Tvätten: \(timeRemainingFormatter(minutesRemaining: washerCompletionInMinutes))")
24 | }
25 |
26 | let dryerCompletionInMinutes = dryerCompletionTime.date.minutesLeft()
27 | if dryerCompletionInMinutes >= 0 && dryerState.state.isRunning() {
28 | text.addNewLineAndAppend("Torktumlaren: \(timeRemainingFormatter(minutesRemaining: dryerCompletionInMinutes))")
29 | }
30 |
31 | if let chargingPower = Double(easeeCharger.state), chargingPower > 0 {
32 | text.addNewLineAndAppend("Laddbox: \(chargingPower.roundedWithOneDecimal)kW")
33 | }
34 |
35 | if let generalWasteDescription = generalWasteDate.date.daysRemainingDescription() {
36 | text.addNewLineAndAppend("Restavfall töms \(generalWasteDescription)")
37 | }
38 | if let plasticWasteDescription = plasticWasteDate.date.daysRemainingDescription() {
39 | text.addNewLineAndAppend("Plast töms \(plasticWasteDescription)")
40 | }
41 | if let gardenWasteDescription = gardenWasteDate.date.daysRemainingDescription() {
42 | text.addNewLineAndAppend("Trädgårdsavfall töms \(gardenWasteDescription)")
43 | }
44 |
45 | return text
46 | }
47 |
48 | private func timeRemainingFormatter(minutesRemaining: Int) -> String {
49 | if minutesRemaining >= 60 {
50 | let hours = minutesRemaining / 60
51 | let minutes = minutesRemaining % 60
52 | return "\(hours)h \(minutes)min"
53 | } else {
54 | return "\(minutesRemaining)min"
55 | }
56 | }
57 | }
58 |
59 | private extension String {
60 | func isRunning() -> Bool {
61 | lowercased() != "none"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Extensions/ImageExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageExtension.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-02-19.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | extension Image {
12 | init(imageName: ImageName) {
13 | self.init(imageName.rawValue)
14 | }
15 |
16 | init(systemImageName: SystemImageName) {
17 | self.init(systemName: systemImageName.rawValue)
18 | }
19 | }
20 |
21 | enum ImageName: String {
22 | case aircondition
23 | case defrost = "defrost.filled"
24 | case evPlugType2 = "ev-plug-type2"
25 | case evPlugCCS2 = "ev-plug-ccs2"
26 | case floorplan
27 | case gym
28 | case hallway
29 | case powerGrid = "powergrid"
30 | case refresh
31 | case settings
32 | case solarPanel = "solarpanel"
33 | case seatHeater = "seatheater.filled"
34 | case vince
35 | case washing
36 | }
37 |
38 | // SF Symbols
39 | enum SystemImageName: String {
40 | case arrowDown = "arrow.down"
41 | case arrowUp = "arrow.up"
42 | case bedDouble = "bed.double"
43 | case bolt = "bolt.fill"
44 | case boltSlash = "bolt.slash"
45 | case boltCar = "bolt.car"
46 | case evCharger = "ev.charger"
47 | case evChargerSlash = "ev.charger.slash"
48 | case cctv = "video.fill"
49 | case clock
50 | case engineFilled = "engine.combustion.fill"
51 | case forkKnife = "fork.knife"
52 | case house = "house.fill"
53 | case iPhone = "iphone"
54 | case iPhoneActive = "iphone.radiowaves.left.and.right"
55 | case locked = "lock.fill"
56 | case lockSlash = "lock.slash.fill"
57 | case pause = "pause.fill"
58 | case pills = "pills.fill"
59 | case play = "play.fill"
60 | case playTV = "play.tv"
61 | case powerplug
62 | case scope
63 | case thermometer
64 | case headLightBeam = "headlight.high.beam"
65 | case lightbulbSlash = "lightbulb.slash"
66 | case trash = "trash.fill"
67 | case unknown = "questionmark.circle"
68 | case unlocked = "lock.open.fill"
69 | case xmarkCircle = "xmark.circle"
70 | }
71 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Extensions/LinearGradientExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LinearGradientExtension.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2024-01-24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension LinearGradient {
11 | static var buttonGradient: LinearGradient {
12 | LinearGradient(gradient: Gradient(colors: [.appIconBlue.opacity(0.5), .appIconGreen.opacity(0.5)]),
13 | startPoint: .leading,
14 | endPoint: .trailing)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Extensions/LogExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LogExtension.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-03-11.
6 | //
7 |
8 | import Foundation
9 | import ShipBookSDK
10 |
11 | extension Log {
12 | @MainActor
13 | static var user: String {
14 | UserManager.currentUser.rawValue
15 | }
16 |
17 | static func info(_ message: String,
18 | tag: String? = nil,
19 | function: String = #function,
20 | file: String = #file,
21 | line: Int = #line) {
22 | Task { @MainActor in
23 | Log.i("\(user): \(message)",
24 | tag: tag,
25 | function: function,
26 | file: file,
27 | line: line)
28 | }
29 | }
30 |
31 | static func error(_ message: String,
32 | tag: String? = nil,
33 | function: String = #function,
34 | file: String = #file,
35 | line: Int = #line) {
36 | Task { @MainActor in
37 | Log.e("\(user): \(message)",
38 | tag: tag,
39 | function: function,
40 | file: file,
41 | line: line)
42 | }
43 | }
44 |
45 | static func warning(_ message: String,
46 | tag: String? = nil,
47 | function: String = #function,
48 | file: String = #file,
49 | line: Int = #line) {
50 | Task { @MainActor in
51 | Log.w("\(user): \(message)",
52 | tag: tag,
53 | function: function,
54 | file: file,
55 | line: line)
56 | }
57 | }
58 |
59 | static func debug(_ message: String,
60 | tag: String? = nil,
61 | function: String = #function,
62 | file: String = #file,
63 | line: Int = #line) {
64 | Task { @MainActor in
65 | Log.d("\(user): \(message)",
66 | tag: tag,
67 | function: function,
68 | file: file,
69 | line: line)
70 | }
71 | }
72 |
73 | static func verbose(_ message: String,
74 | tag: String? = nil,
75 | function: String = #function,
76 | file: String = #file,
77 | line: Int = #line) {
78 | Task { @MainActor in
79 | Log.v("\(user): \(message)",
80 | tag: tag,
81 | function: function,
82 | file: file,
83 | line: line)
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Extensions/StringExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StringExtension.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-11-22.
6 | //
7 |
8 | import Foundation
9 |
10 | extension String {
11 | var removingHTTPSchemeAndTrailingSlash: String {
12 | replacingOccurrences(of: "http://", with: "").replacingOccurrences(of: "https://", with: "").removingTrailingSlash
13 | }
14 |
15 | var removingTrailingSlash: String {
16 | if hasSuffix("/") {
17 | return String(dropLast())
18 | }
19 | return self
20 | }
21 |
22 | var toKW: String {
23 | if let doubleValue = Double(self) {
24 | doubleValue.toKWString
25 | } else {
26 | "?kW"
27 | }
28 | }
29 |
30 | var toKWh: String {
31 | if let kiloWattHours = Double(self) {
32 | let rounded = kiloWattHours.roundedWithOneDecimal
33 | return kiloWattHours == 0 ? "\(Int(rounded))kWh" : String(format: "%.1fkWh", rounded)
34 | } else {
35 | return "?kWh"
36 | }
37 | }
38 |
39 | var toOre: String {
40 | if let doubleValue = Double(self) {
41 | let ore = Int(round(doubleValue * 100))
42 | return "\(ore) Öre"
43 | } else {
44 | return "? Öre"
45 | }
46 | }
47 |
48 | var toKr: String {
49 | if let doubleValue = Double(self) {
50 | "\(doubleValue.roundedWithOneDecimal) Kr"
51 | } else {
52 | "? Kr"
53 | }
54 | }
55 |
56 | var roundedWithOneDecimal: Double {
57 | if let doubleValue = Double(self) {
58 | doubleValue.roundedWithOneDecimal
59 | } else {
60 | 0
61 | }
62 | }
63 |
64 | mutating func addNewLineAndAppend(_ other: String) {
65 | if isNotEmpty {
66 | append("\n")
67 | }
68 |
69 | append(other)
70 | }
71 |
72 | var removeDoubleSpaces: String {
73 | replacingOccurrences(of: " {2,}", with: " ", options: .regularExpression)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Extensions/TaskExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TaskExtension.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-10-01.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Task where Success == Never, Failure == Never {
11 | static func sleep(seconds: Double) async throws {
12 | let duration = UInt64(seconds * 1_000_000_000)
13 | try await Task.sleep(nanoseconds: duration)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Extensions/UINavigationController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UINavigationController.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-05-02.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | extension UINavigationController: @retroactive UIGestureRecognizerDelegate {
12 | override open func viewDidLoad() {
13 | super.viewDidLoad()
14 | interactivePopGestureRecognizer?.delegate = self
15 | }
16 |
17 | public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
18 | viewControllers.count > 1
19 | }
20 |
21 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
22 | shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
23 | true
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Extensions/UserDefaultsExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaultsExtension.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2024-01-24.
6 | //
7 |
8 | import Foundation
9 | import ShipBookSDK
10 |
11 | extension UserDefaults {
12 | @MainActor
13 | func setCoordinates(_ coordinates: Coordinates, forKey key: StorageKeys) {
14 | do {
15 | let encodedData = try JSONEncoder().encode(coordinates)
16 | set(encodedData, forKey: key.rawValue)
17 | } catch {
18 | Log.error("Failed to encode coordinates: \(error)")
19 | }
20 | }
21 |
22 | func coordinates(forKey key: String) -> Coordinates? {
23 | guard let data = data(forKey: key) else {
24 | return nil
25 | }
26 |
27 | return try? JSONDecoder().decode(Coordinates.self, from: data)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Extensions/UserDefaultsExtensionShared.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaultsExtensionShared.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2024-01-19.
6 | //
7 |
8 | import Foundation
9 | import ShipBookSDK
10 |
11 | extension UserDefaults {
12 | @MainActor
13 | static let shared: UserDefaults = {
14 | guard let sharedDefaults = UserDefaults(suiteName: "group.se.laross.intellinest.shared") else {
15 | return UserDefaults.standard
16 | }
17 | return sharedDefaults
18 | }()
19 | }
20 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Extensions/ViewExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewExtension.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2024-01-24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension View {
11 | func backgroundModifier() -> some View {
12 | let gradients = Gradient(stops: [.init(color: .appIconDark.opacity(0.2), location: 0),
13 | .init(color: .appIconBlue.opacity(0.4), location: 0.2),
14 | .init(color: .appIconBlue.opacity(0.5), location: 0.78),
15 | .init(color: .appIconBlue.opacity(0.5), location: 0.9),
16 | .init(color: .appIconGreen.opacity(0.4), location: 1.1)])
17 | return background(
18 | ZStack {
19 | Color.black
20 | LinearGradient(gradient: gradients, startPoint: .top, endPoint: .bottom)
21 | }
22 | .edgesIgnoringSafeArea(.all)
23 | )
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/Extensions/ViewModels/HomeViewModelExtension+LockServiceProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeViewModelExtension+LockServiceProtocol.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-05-23.
6 | //
7 |
8 | import Foundation
9 | import ShipBookSDK
10 |
11 | extension HomeViewModel: LockServiceProtocol {
12 | func toggleStateForSideDoor() {
13 | guard let capturedLock = getUpdatedLock(sideDoor) else {
14 | return
15 | }
16 | Task { @MainActor in
17 | let action: Action = sideDoor.lockState == .unlocked ? .lock : .unlock
18 | sideDoor.expectedState = capturedLock.expectedState
19 | let success = await yaleApiService.setLockState(lockID: capturedLock.id, action: action)
20 | if success {
21 | sideDoor.lockState = capturedLock.expectedState
22 | }
23 | sideDoor.expectedState = .unknown
24 | }
25 | }
26 |
27 | func toggleStateForFrontDoor() {
28 | guard let capturedLock = getUpdatedLock(frontDoor) else {
29 | return
30 | }
31 | Task { @MainActor in
32 | let action: Action = frontDoor.lockState == .unlocked ? .lock : .unlock
33 | frontDoor.expectedState = capturedLock.expectedState
34 | let success = await yaleApiService.setLockState(lockID: capturedLock.id, action: action)
35 | if success {
36 | frontDoor.lockState = capturedLock.expectedState
37 | }
38 | frontDoor.expectedState = .unknown
39 | }
40 | }
41 |
42 | private func getUpdatedLock(_ lock: Lockable) -> Lockable? {
43 | var lockToUpdate = lock
44 | switch lock.lockState {
45 | case .unlocked:
46 | lockToUpdate.expectedState = .locked
47 | case .locked:
48 | lockToUpdate.expectedState = .unlocked
49 | default:
50 | Log.warning("Trying to toggle from bad initial state: \(lock.lockState)")
51 | return nil
52 | }
53 |
54 | return lockToUpdate
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/IntelliNest/Utils/SSIDUtil.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SSIDUtil.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-11-24.
6 | //
7 |
8 | @preconcurrency import CoreLocation
9 | import Foundation
10 | import NetworkExtension
11 | import ShipBookSDK
12 | import SystemConfiguration.CaptiveNetwork
13 |
14 | struct SSIDUtil {
15 | private static let locationManager = CLLocationManager()
16 | @MainActor private static let locationManagerDelegate = LocationManagerDelegate()
17 | private class LocationManagerDelegate: NSObject, CLLocationManagerDelegate {
18 | var permissionHandler: ((Bool) -> Void)?
19 |
20 | func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
21 | switch status {
22 | case .authorizedWhenInUse, .authorizedAlways:
23 | permissionHandler?(true)
24 | default:
25 | permissionHandler?(false)
26 | }
27 | }
28 | }
29 |
30 | static func getCurrentSSID() async -> String? {
31 | let isAuthorized = await requestLocationPermission()
32 | guard isAuthorized, let currentNetwork = await NEHotspotNetwork.fetchCurrent() else {
33 | return nil
34 | }
35 | return currentNetwork.ssid
36 | }
37 |
38 | @MainActor
39 | private static func requestLocationPermission() async -> Bool {
40 | await withCheckedContinuation { continuation in
41 | locationManager.delegate = locationManagerDelegate
42 |
43 | let status = locationManager.authorizationStatus
44 | switch status {
45 | case .authorizedWhenInUse, .authorizedAlways:
46 | continuation.resume(returning: true)
47 | case .notDetermined:
48 | locationManagerDelegate.permissionHandler = { granted in
49 | continuation.resume(returning: granted)
50 | }
51 | locationManager.requestAlwaysAuthorization()
52 | default:
53 | continuation.resume(returning: false)
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/IntelliNest/ViewModels/HeatersViewModelExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HeatersViewModelExtension.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-06-17.
6 | //
7 |
8 | import Foundation
9 | import ShipBookSDK
10 |
11 | extension HeatersViewModel {
12 | @MainActor
13 | func reload() async {
14 | async let tmpThermCorridor = reload(entity: thermCorridor)
15 | async let tmpThermBedroom = reload(entity: thermBedroom)
16 | async let tmpThermGym = reload(entity: thermGym)
17 | async let tmpThermVince = reload(entity: thermVince)
18 | async let tmpThermKitchen = reload(entity: thermKitchen)
19 | async let tmpThermCommonarea = reload(entity: thermCommonarea)
20 | async let tmpThermPlayroom = reload(entity: thermPlayroom)
21 | async let tmpThermGuest = reload(entity: thermGuest)
22 | async let tmpHeaterCorridor = reload(entity: heaterCorridor)
23 | async let tmpHeaterPlayroom = reload(entity: heaterPlayroom)
24 |
25 | thermCorridor = await tmpThermCorridor
26 | thermBedroom = await tmpThermBedroom
27 | thermGym = await tmpThermGym
28 | thermVince = await tmpThermVince
29 | thermKitchen = await tmpThermKitchen
30 | thermCommonarea = await tmpThermCommonarea
31 | thermPlayroom = await tmpThermPlayroom
32 | thermGuest = await tmpThermGuest
33 | heaterCorridor = await tmpHeaterCorridor
34 | heaterPlayroom = await tmpHeaterPlayroom
35 |
36 | for entityID in entityIDs {
37 | do {
38 | if entityID == .purifierFanSpeed {
39 | let purifierSpeed = try await restAPIService.reload(entityId: entityID, entityType: PurifierSpeed.self)
40 | purifier.speed = purifierSpeed.speed
41 | } else {
42 | let entity = try await restAPIService.reloadState(entityID: entityID)
43 | reload(entityID: entityID, state: entity.state)
44 | }
45 | } catch {
46 | Log.error("Failed to reload entity: \(entityID): \(error)")
47 | }
48 | }
49 | }
50 |
51 | func reload(entity: T) async -> T {
52 | await restAPIService.reload(hassEntity: entity, entityType: T.self)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/IntelliNest/ViewModels/LockServiceProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LockServiceProtocol.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-05-23.
6 | //
7 |
8 | import Foundation
9 | import ShipBookSDK
10 |
11 | @MainActor
12 | protocol LockServiceProtocol {
13 | var sideDoor: YaleLock { get set }
14 | var frontDoor: YaleLock { get set }
15 | var yaleApiService: YaleApiService { get }
16 | func toggleStateForSideDoor()
17 | func toggleStateForFrontDoor()
18 | }
19 |
--------------------------------------------------------------------------------
/IntelliNest/ViewModels/VLCMediaplayerMock.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VLCMediaplayerMock.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-06-10.
6 | //
7 |
8 | import Foundation
9 |
10 | class VLCMediaMock {
11 | init(url: URL) {}
12 | func addOptions(_ options: [String: Int]) {}
13 | }
14 |
15 | protocol VLCMediaPlayerMockDelegate: AnyObject {}
16 |
17 | enum VLCState {
18 | case playing
19 | }
20 |
21 | class VLCMediaPlayerMock {
22 | var drawable: Any?
23 | var state: VLCState?
24 | var delegate: VLCMediaPlayerMockDelegate?
25 | var media: VLCMediaMock?
26 |
27 | func play() {}
28 | func pause() {}
29 | }
30 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Components/BatteryView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BatteryView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-02-09.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct BatteryView: View {
11 | var level: Int
12 | var isCharging: Bool
13 | var timeUntilCharged: Int?
14 | var degreeRotation: Double
15 | var width: CGFloat
16 | var height: CGFloat
17 | private let batteryCornerRadius: CGFloat = 15
18 |
19 | init(level: Int,
20 | isCharging: Bool,
21 | timeUntilCharged: Int? = nil,
22 | degreeRotation: Double = 0,
23 | width: CGFloat = 50,
24 | height: CGFloat = 90) {
25 | self.level = level
26 | self.isCharging = isCharging
27 | self.timeUntilCharged = timeUntilCharged
28 | self.degreeRotation = degreeRotation
29 | self.width = width
30 | self.height = height
31 | }
32 |
33 | var body: some View {
34 | ZStack {
35 | Group {
36 | VStack {
37 | Rectangle()
38 | .fill(level > 99 ? Color.green : Color.gray.opacity(0.4))
39 | .frame(width: 0.25 * width, height: 0.07 * height, alignment: .center)
40 | .padding(.bottom, -4)
41 | Rectangle()
42 | .fill(Color.gray.opacity(0.4))
43 | .frame(width: width, height: height, alignment: .bottom)
44 | .cornerRadius(batteryCornerRadius)
45 | .padding(.top, -4)
46 | }
47 | Rectangle()
48 | .fill(level > 60 ? .green : level > 30 ? .yellow : .red)
49 | .frame(width: width, height: height, alignment: .bottom)
50 | .scaleEffect(CGSize(width: 1, height: CGFloat(level) / 100.0), anchor: .bottom)
51 | .cornerRadius(batteryCornerRadius)
52 | .padding(.bottom, -5)
53 | }
54 | .rotationEffect(.degrees(degreeRotation))
55 | VStack {
56 | if isCharging {
57 | Image(systemName: "bolt")
58 | .foregroundColor(.yellow)
59 | if let timeUntilCharged {
60 | Text("\(timeUntilCharged)min")
61 | .font(.buttonFontSmall)
62 | .minimumScaleFactor(0.2)
63 | .lineLimit(1)
64 | }
65 | }
66 | Text("\(level)%")
67 | .font(Font.headline.weight(.semibold))
68 | .lineLimit(1)
69 | .minimumScaleFactor(0.2)
70 | .foregroundColor(.white)
71 | }
72 | }
73 | }
74 | }
75 |
76 | struct BatteryView_Previews: PreviewProvider {
77 | static var previews: some View {
78 | HStack {
79 | BatteryView(level: 62, isCharging: false)
80 | BatteryView(level: 99, isCharging: false)
81 | BatteryView(level: 100, isCharging: false)
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Components/ErrorBannerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ErrorBannerView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2024-01-31.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ErrorBannerView: View {
11 | let title: String
12 | let message: String
13 |
14 | var body: some View {
15 | RoundedRectangle(cornerRadius: 10)
16 | .frame(height: 60)
17 | .foregroundStyle(Color.blend(.red, with: .black, ratio: 0.3))
18 | .background(Color.clear)
19 | .shadow(radius: 5)
20 | .padding(.horizontal, 2)
21 | .overlay {
22 | VStack {
23 | Group {
24 | Text(title)
25 | .font(.buttonFontLarge)
26 | .lineLimit(1)
27 | .minimumScaleFactor(0.2)
28 | Text(message)
29 | .font(.buttonFontMedium)
30 | .lineLimit(2)
31 | .multilineTextAlignment(.center)
32 | .minimumScaleFactor(0.1)
33 | }
34 | .foregroundColor(.white)
35 | .background(Color.clear)
36 | }
37 | }
38 | }
39 | }
40 |
41 | #Preview {
42 | VStack {
43 | ErrorBannerView(title: "Failed to send bla bla",
44 | message: "Status code: 502")
45 | Spacer()
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Components/FuelView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct FuelView: View {
4 | var level: Int
5 | var maxLevel: Double
6 | var degreeRotation: Double
7 | var width: CGFloat
8 | var height: CGFloat
9 |
10 | private let fuelCornerRadius: CGFloat = 15
11 |
12 | init(level: Int, maxLevel: Int = 42, degreeRotation: Double = 0, width: CGFloat = 50, height: CGFloat = 90) {
13 | self.level = level
14 | self.maxLevel = Double(maxLevel)
15 | self.degreeRotation = degreeRotation
16 | self.width = width
17 | self.height = height
18 | }
19 |
20 | var body: some View {
21 | ZStack {
22 | Group {
23 | Rectangle()
24 | .fill(Color.gray.opacity(0.4))
25 | .frame(width: width, height: height, alignment: .bottom)
26 | .cornerRadius(fuelCornerRadius)
27 | Rectangle()
28 | .fill(Double(level) > 0.6 * maxLevel ? .green : Double(level) > 0.3 * maxLevel ? .yellow : .red)
29 | .frame(width: width, height: height, alignment: .bottom)
30 | .scaleEffect(CGSize(width: 1, height: CGFloat(level) / maxLevel), anchor: .bottom)
31 | .cornerRadius(fuelCornerRadius)
32 | }
33 | .rotationEffect(.degrees(degreeRotation))
34 |
35 | Text("\(level)L")
36 | .font(Font.headline.weight(.semibold))
37 | .lineLimit(1)
38 | .minimumScaleFactor(0.2)
39 | .foregroundColor(.white)
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Components/INText.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct INText: View {
4 | let text: String
5 | let foregroundStyle: Color
6 | let font: Font
7 | let lineLimit: Int
8 | let minimumScaleFactor: CGFloat
9 | let multilineTextAlignment: TextAlignment
10 | let maxWidth: CGFloat?
11 | let maxHeight: CGFloat?
12 | let horizontalPadding: CGFloat
13 | let verticalPadding: CGFloat
14 | var trimmedText: String {
15 | text.removeDoubleSpaces
16 | }
17 |
18 | let options = AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace)
19 | var attributedText: AttributedString {
20 | (try? AttributedString(markdown: trimmedText, options: options)) ?? AttributedString(trimmedText)
21 | }
22 |
23 | var body: some View {
24 | Text(attributedText)
25 | .multilineTextAlignment(multilineTextAlignment)
26 | .minimumScaleFactor(minimumScaleFactor)
27 | .font(font)
28 | .foregroundStyle(foregroundStyle)
29 | .padding(.horizontal, horizontalPadding)
30 | .padding(.vertical, verticalPadding)
31 | .lineLimit(lineLimit)
32 | .frame(maxWidth: maxWidth, maxHeight: maxHeight)
33 | .truncationMode(.tail)
34 | }
35 |
36 | init(_ text: String,
37 | foregroundStyle: Color = .white,
38 | font: Font = .body,
39 | lineLimit: Int = .max,
40 | multilineTextAlignment: TextAlignment = .center,
41 | minimumScaleFactor: CGFloat = 0.1,
42 | maxWidth: CGFloat? = nil,
43 | maxHeight: CGFloat? = nil,
44 | horizontalPadding: CGFloat = 0,
45 | verticalPadding: CGFloat = 0) {
46 | self.text = text
47 | self.font = font
48 | self.foregroundStyle = foregroundStyle
49 | self.lineLimit = lineLimit
50 | self.multilineTextAlignment = multilineTextAlignment
51 | self.minimumScaleFactor = minimumScaleFactor
52 | self.maxWidth = maxWidth
53 | self.maxHeight = maxHeight
54 | self.horizontalPadding = horizontalPadding
55 | self.verticalPadding = verticalPadding
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Components/NavigationButtonView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigationButtonView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2024-01-17.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NavigationButtonView: View {
11 | @State private var tapped = false
12 |
13 | var buttonTitle: String
14 | var image: Image
15 | let buttonImageWidth: CGFloat
16 | let buttonImageHeight: CGFloat
17 | let frameSize: CGFloat
18 | let isActive: Bool
19 | let action: MainActorVoidClosure
20 |
21 | init(buttonTitle: String = "",
22 | image: Image,
23 | buttonImageWidth: CGFloat = dashboardButtonImageSize,
24 | buttonImageHeight: CGFloat = dashboardButtonImageSize,
25 | frameSize: CGFloat = dashboardButtonFrameWidth,
26 | isActive: Bool = false,
27 | action: @escaping MainActorVoidClosure = {}) {
28 | self.buttonTitle = buttonTitle
29 | self.image = image
30 | self.buttonImageWidth = buttonImageWidth
31 | self.buttonImageHeight = buttonImageHeight
32 | self.frameSize = frameSize
33 | self.isActive = isActive
34 | self.action = action
35 | }
36 |
37 | var body: some View {
38 | Button {
39 | action()
40 | withAnimation(.spring()) {
41 | tapped = true
42 | Task { @MainActor in
43 | try? await Task.sleep(seconds: 0.1)
44 | tapped = false
45 | }
46 | }
47 | } label: {
48 | VStack {
49 | image
50 | .resizable()
51 | .frame(width: buttonImageWidth, height: buttonImageHeight)
52 | .foregroundColor(isActive ? .yellow : .white)
53 | if buttonTitle.isNotEmpty {
54 | Text(buttonTitle)
55 | .font(.buttonFontMedium)
56 | .foregroundColor(.white)
57 | }
58 | }
59 | .frame(width: frameSize, height: frameSize, alignment: .center)
60 | .background(LinearGradient.buttonGradient)
61 | .cornerRadius(dashboardButtonCornerRadius)
62 | }
63 | }
64 | }
65 |
66 | #Preview {
67 | NavigationButtonView(buttonTitle: "Test", image: Image(systemName: "bolt"))
68 | }
69 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Components/PrimaryContentBorderView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrimaryContentBorderView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2024-01-25.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct PrimaryContentBorderView: View {
11 | var isSelected: Bool
12 |
13 | var body: some View {
14 | RoundedRectangle(cornerRadius: dashboardButtonCornerRadius)
15 | .stroke(isSelected ? Color.primaryContentSelectedBorder : Color.primaryContentBorder,
16 | lineWidth: isSelected ? 2 : 3)
17 | }
18 | }
19 |
20 | #Preview {
21 | PrimaryContentBorderView(isSelected: false)
22 | }
23 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Components/VerticalDivider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VerticalDivider.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-02-09.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct VerticalDivider: View {
11 | let color: Color = .gray
12 | let width: CGFloat = 2
13 | var body: some View {
14 | Rectangle()
15 | .fill(color)
16 | .frame(width: width)
17 | .edgesIgnoringSafeArea(.horizontal)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Components/ZoomableImageView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ZoomableImageView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-06-09.
6 | //
7 |
8 | import SwiftUI
9 | import UIKit
10 |
11 | struct ZoomableImageView: View {
12 | var image: Image
13 | let isPortrait: Bool
14 | @Binding var isFullScreen: Bool
15 | @State var initialScale: CGFloat
16 | @State private var initialOffset = CGSize.zero
17 | @State private var accumulatedScale: CGFloat = 1.0
18 | @State private var latestScale: CGFloat = 1.0
19 |
20 | var body: some View {
21 | GeometryReader { geometry in
22 | Group {
23 | if isPortrait {
24 | image
25 | .resizable()
26 | .scaleEffect(accumulatedScale * latestScale)
27 | .frame(width: geometry.size.width, height: geometry.size.height)
28 | } else {
29 | image
30 | .resizable()
31 | .aspectRatio(contentMode: .fill)
32 | .scaleEffect(accumulatedScale * latestScale)
33 | .frame(width: geometry.size.width, height: geometry.size.height)
34 | .rotationEffect(.degrees(90))
35 | }
36 | }
37 | .gesture(
38 | MagnificationGesture()
39 | .onChanged { value in
40 | latestScale = value.magnitude
41 | }
42 | .onEnded { _ in
43 | accumulatedScale *= latestScale
44 | latestScale = 1.0
45 | },
46 | including: .all
47 | )
48 | .onTapGesture {
49 | isFullScreen = false
50 | }
51 | .onAppear {
52 | accumulatedScale = initialScale
53 | }
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Dashboards/ElectricityView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ElectricityView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-12-15.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ElectricityView: View {
11 | @ObservedObject var viewModel: ElectricityViewModel
12 |
13 | var body: some View {
14 | ZStack {
15 | VStack {
16 | HStack(alignment: .top) {
17 | ElectricityFlowView(viewModel: viewModel)
18 | .frame(width: 230)
19 | .padding([.top, .leading], 16)
20 | Group {
21 | Text("Kostnad idag: ") + Text("\(viewModel.tibberCostToday.state.toKr)").bold() +
22 | Text("\nKöpt idag: ") + Text("\(viewModel.pulseConsumptionToday.state.toKWh)").bold() +
23 | Text("\nMode: ") + Text("\(viewModel.sonnenBattery.operationMode.title)").bold()
24 | }
25 | .font(.buttonFontMedium)
26 | .lineLimit(3)
27 | .minimumScaleFactor(0.1)
28 | .padding(.top, 8)
29 | Spacer()
30 | }
31 | Spacer()
32 | NordPoolHistoryView(nordPool: $viewModel.nordPool)
33 | .frame(height: 350)
34 | .padding(.bottom, 16)
35 | .padding(.horizontal, 8)
36 | }
37 |
38 | if viewModel.isShowingSonnenSettings {
39 | SonnenSettingsView(viewModel: viewModel)
40 | }
41 | }
42 | .foregroundStyle(.white)
43 | }
44 | }
45 |
46 | #Preview {
47 | VStack {
48 | ElectricityView(viewModel: PreviewProviderUtil.electricityViewModel)
49 | .backgroundModifier()
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Dashboards/HeatersView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HeatersView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-02-02.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct HeatersView: View {
11 | @ObservedObject var viewModel: HeatersViewModel
12 |
13 | var body: some View {
14 | ScrollView {
15 | VStack {
16 | SimpleHeaterView(roomName: "Korridoren",
17 | therm1: viewModel.thermCorridor,
18 | therm2: viewModel.thermBedroom,
19 | therm3: viewModel.thermVince,
20 | therm4: viewModel.thermGym,
21 | heater: $viewModel.heaterCorridor,
22 | resetClimateTimeEntity: $viewModel.resetCorridorHeaterTime,
23 | isTimerModeEnabled: viewModel.heaterCorridorTimerMode.isActive,
24 | showDetailsClosure: viewModel.showHeaterDetails,
25 | setTargetTemperatureClosure: viewModel.setTargetTemperature,
26 | setHvacModeClosure: viewModel.setHvacMode,
27 | toggleTimerModeClosure: viewModel.toggleCorridorTimerMode,
28 | setClimateScheduleTimeClosure: viewModel.setClimateSchedule)
29 | .padding(.top)
30 | Divider()
31 | SimpleHeaterView(roomName: "Lekrummet",
32 | therm1: viewModel.thermPlayroom,
33 | therm2: viewModel.thermCommonarea,
34 | therm3: viewModel.thermGuest,
35 | therm4: viewModel.thermKitchen,
36 | heater: $viewModel.heaterPlayroom,
37 | resetClimateTimeEntity: $viewModel.resetPlayroomHeaterTime,
38 | isTimerModeEnabled: viewModel.heaterPlayroomTimerMode.isActive,
39 | showDetailsClosure: viewModel.showHeaterDetails,
40 | setTargetTemperatureClosure: viewModel.setTargetTemperature,
41 | setHvacModeClosure: viewModel.setHvacMode,
42 | toggleTimerModeClosure: viewModel.togglePlayroomTimerMode,
43 | setClimateScheduleTimeClosure: viewModel.setClimateSchedule)
44 | .padding(.bottom)
45 | Divider()
46 | PurifierView(purifier: $viewModel.purifier,
47 | resetClimateTimeEntity: $viewModel.resetPurifierTime,
48 | isTimerModeEnabled: viewModel.purifierTimerMode.isActive,
49 | setFanSpeedClosure: viewModel.setPurifierFanSpeed,
50 | toggleTimerModeClosure: viewModel.togglePurifierTimerMode,
51 | setClimateScheduleTimeClosure: viewModel.setClimateSchedule)
52 | .padding(.bottom)
53 | }
54 | }
55 | }
56 | }
57 |
58 | struct HeatersView_Previews: PreviewProvider {
59 | static var previews: some View {
60 | HeatersView(viewModel: HeatersViewModel(restAPIService: PreviewProviderUtil.restAPIService,
61 | showHeaterDetails: { _ in }))
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Dashboards/LeafView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct LeafView: View {
4 | @ObservedObject var viewModel: LynkViewModel
5 | @State var isEngineAlertVisible = false
6 |
7 | var body: some View {
8 | VStack {
9 | INText("Leaf", font: .title3)
10 | INText(viewModel.leafLastPoll.date.humanReadable, font: .buttonFontExtraSmall)
11 | .padding(.bottom)
12 | HStack {
13 | Spacer()
14 | VStack {
15 | BatteryView(level: Int(viewModel.leafBattery.inputNumber.rounded()),
16 | isCharging: viewModel.isLeafCharging.isActive,
17 | degreeRotation: 90,
18 | width: 50,
19 | height: 90)
20 | INText("\(viewModel.leafRangeAC.state)km", font: .buttonFontSmall)
21 | .padding(.top, -32)
22 | }
23 | .padding(.trailing, 32)
24 |
25 | VStack {
26 | ServiceButtonView(buttonTitle: viewModel.leafClimateTitle,
27 | isActive: viewModel.isLeafAirConditionActive,
28 | buttonSize: viewModel.buttonSize,
29 | icon: .init(systemImageName: .thermometer),
30 | iconWidth: 25,
31 | iconHeight: 35,
32 | isLoading: viewModel.isLeafAirConditionLoading,
33 | action: viewModel.toggleLeafClimate)
34 | .contextMenu {
35 | Button(action: viewModel.startLeafClimate) {
36 | INText("Starta")
37 | }
38 | Button(action: viewModel.stopLeafClimate) {
39 | INText("Stoppa")
40 | }
41 | }
42 | if let minutes = viewModel.leafClimateTimerRemaining {
43 | INText("\(minutes)min")
44 | }
45 | }
46 | Spacer()
47 | }
48 | Spacer()
49 | }
50 | }
51 | }
52 |
53 | struct Leaf_Previews: PreviewProvider {
54 | static var previews: some View {
55 | LeafView(viewModel: PreviewProviderUtil.lynkViewModel)
56 | .backgroundModifier()
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Electricity/FlowIndicatorView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct FlowIndicatorView: View {
4 | @Binding var isFlowing: Bool
5 | var flowIntensity: Double
6 | let arrowCount: Int
7 |
8 | var body: some View {
9 | if isFlowing {
10 | TimelineView(.animation(minimumInterval: nil)) { timeline in
11 | let date = timeline.date.timeIntervalSinceReferenceDate
12 | let phase = date * flowIntensity
13 |
14 | HStack {
15 | ForEach(0 ..< arrowCount, id: \.self) { index in
16 | ArrowShape()
17 | .frame(width: 12, height: 6)
18 | .foregroundStyle(.yellow)
19 | .opacity(arrowOpacity(phase: phase, index: index))
20 | }
21 | }
22 | }
23 | }
24 | }
25 |
26 | private func arrowOpacity(phase: Double, index: Int) -> Double {
27 | guard arrowCount > 0 else { return 0 }
28 | let position = (phase - Double(index) / Double(arrowCount)).truncatingRemainder(dividingBy: 1.0)
29 | return max(0, sin(position * 2 * .pi))
30 | }
31 | }
32 |
33 | private struct ArrowShape: Shape {
34 | func path(in rect: CGRect) -> Path {
35 | var path = Path()
36 | path.move(to: .zero)
37 | path.addLine(to: CGPoint(x: rect.width, y: rect.height / 2))
38 | path.addLine(to: CGPoint(x: 0, y: rect.height))
39 | path.addLine(to: CGPoint(x: rect.width / 4, y: rect.height / 2))
40 | path.closeSubpath()
41 | return path
42 | }
43 | }
44 |
45 | #Preview {
46 | FlowIndicatorView(isFlowing: .constant(true), flowIntensity: 1, arrowCount: 3)
47 | }
48 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Heater/DetailedHeaterView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailedHeaterView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-06-22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | enum HeaterType {
11 | case corridor
12 | case playroom
13 | }
14 |
15 | @MainActor
16 | struct DetailedHeaterView: View {
17 | @ObservedObject var viewModel: HeatersViewModel
18 | var selectedHeater: HeaterType
19 |
20 | var heater: HeaterEntity {
21 | selectedHeater == .corridor ? viewModel.heaterCorridor : viewModel.heaterPlayroom
22 | }
23 |
24 | var body: some View {
25 | VStack {
26 | Text("Fläkt")
27 | .font(.title2)
28 | .foregroundColor(.white)
29 | FanModeView(fanMode: heater.fanMode,
30 | fanModeSelectedCallback: { fanMode in
31 | viewModel.setFanMode(heater, fanMode)
32 | })
33 | .padding(.bottom)
34 |
35 | Text("Horisontellt läge")
36 | .font(.title2)
37 | .foregroundColor(.white)
38 | HorizontalModeView(mode: heater.vaneHorizontal,
39 | leftVaneTitle: heater.leftVaneTitle,
40 | rightVaneTitle: heater.rightVaneTitle,
41 | horizontalModeSelectedCallback: { horizontalMode in
42 | viewModel.horizontalModeSelectedCallback(heater, horizontalMode)
43 | })
44 | .padding(.bottom)
45 |
46 | Text("Vertikalt läge")
47 | .font(.title2)
48 | .foregroundColor(.white)
49 | VerticalPositionView(mode: heater.vaneVertical,
50 | verticalModeSelectedCallback: { verticalMode in
51 | viewModel.verticalModeSelectedCallback(heater, verticalMode)
52 | })
53 | Spacer()
54 | }
55 | .padding()
56 | .backgroundModifier()
57 | }
58 | }
59 |
60 | struct DetailedHeaterView_Previews: PreviewProvider {
61 | static var previews: some View {
62 | DetailedHeaterView(viewModel: PreviewProviderUtil.heatersViewModel, selectedHeater: .corridor)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Heater/FanModeButtonView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FanModeButtonView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-11-10.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct FanModeButtonView: View {
11 | let fanButtonSize: CGFloat = 50
12 | let fanButtonCornerRadius: CGFloat = 10
13 | let imageSize: CGFloat = 15
14 | var isSelectedFanMode: Bool {
15 | fanMode == selectedFanMode
16 | }
17 |
18 | var fanMode: HeaterFanMode
19 | var selectedFanMode: HeaterFanMode
20 | var image: Image?
21 | var fanModeSelectedCallback: FanModeClosure
22 |
23 | var body: some View {
24 | Button {
25 | fanModeSelectedCallback(fanMode)
26 | } label: {
27 | Group {
28 | if let image {
29 | VStack {
30 | image
31 | .resizable()
32 | .frame(width: imageSize, height: imageSize, alignment: .center)
33 | .colorMultiply(isSelectedFanMode ? .yellow : .white)
34 | .padding(.top)
35 | Text(fanMode.rawValue.capitalized)
36 | .font(.buttonFontLarge.bold())
37 | .padding(.bottom)
38 | }
39 | } else {
40 | Text(fanMode.rawValue)
41 | .font(isSelectedFanMode ? .buttonFontExtraLarge.bold() : .buttonFontExtraLarge)
42 | }
43 | }
44 | .frame(width: fanButtonSize, height: fanButtonSize, alignment: .center)
45 | .foregroundStyle(isSelectedFanMode ? .yellow : .white)
46 | .overlay {
47 | PrimaryContentBorderView(isSelected: isSelectedFanMode)
48 | }
49 | }
50 | }
51 | }
52 |
53 | #Preview {
54 | HStack {
55 | FanModeButtonView(fanMode: .auto, selectedFanMode: .one, image: .init(imageName: .refresh), fanModeSelectedCallback: { _ in })
56 | FanModeButtonView(fanMode: .auto, selectedFanMode: .auto, image: .init(imageName: .refresh), fanModeSelectedCallback: { _ in })
57 | FanModeButtonView(fanMode: .one, selectedFanMode: .one, fanModeSelectedCallback: { _ in })
58 | FanModeButtonView(fanMode: .two, selectedFanMode: .one, fanModeSelectedCallback: { _ in })
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Heater/FanModeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FanModeView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-06-22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct FanModeView: View {
11 | var fanMode: HeaterFanMode
12 | var fanModeSelectedCallback: FanModeClosure
13 |
14 | var body: some View {
15 | HStack {
16 | FanModeButtonView(fanMode: .auto,
17 | selectedFanMode: fanMode,
18 | image: .init(imageName: .refresh),
19 | fanModeSelectedCallback: fanModeSelectedCallback)
20 | FanModeButtonView(fanMode: .one, selectedFanMode: fanMode, fanModeSelectedCallback: fanModeSelectedCallback)
21 | FanModeButtonView(fanMode: .two, selectedFanMode: fanMode, fanModeSelectedCallback: fanModeSelectedCallback)
22 | FanModeButtonView(fanMode: .three, selectedFanMode: fanMode, fanModeSelectedCallback: fanModeSelectedCallback)
23 | FanModeButtonView(fanMode: .four, selectedFanMode: fanMode, fanModeSelectedCallback: fanModeSelectedCallback)
24 | FanModeButtonView(fanMode: .five, selectedFanMode: fanMode, fanModeSelectedCallback: fanModeSelectedCallback)
25 | }
26 | }
27 | }
28 |
29 | struct FanModeView_Previews: PreviewProvider {
30 | static var previews: some View {
31 | FanModeView(fanMode: .auto, fanModeSelectedCallback: { _ in })
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Heater/HorizontalButtonView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HorizontalButtonView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-11-10.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct HorizontalButtonView: View {
11 | let horizontalButtonWidth: CGFloat = 50
12 | let horizontalButtonHeight: CGFloat = 50
13 | let horizontalButtonCornerRadius: CGFloat = 10
14 | var isSelectedMode: Bool {
15 | mode == selectedMode
16 | }
17 |
18 | let mode: HeaterHorizontalMode
19 | let selectedMode: HeaterHorizontalMode
20 | let buttonTitle: String?
21 | let buttonImageName: String?
22 | var horizontalModeSelectedCallback: HorizontalModeClosure
23 |
24 | var body: some View {
25 | Button {
26 | horizontalModeSelectedCallback(mode)
27 | } label: {
28 | Group {
29 | if let buttonTitle {
30 | if buttonTitle.count > 5 {
31 | Text(buttonTitle)
32 | .font(.caption)
33 | .frame(width: 100, height: horizontalButtonHeight, alignment: .center)
34 | } else if buttonTitle.count > 1 {
35 | Text(buttonTitle).font(.body)
36 | .frame(width: 60, height: horizontalButtonHeight, alignment: .center)
37 | } else {
38 | Text(buttonTitle).font(.title)
39 | .frame(width: horizontalButtonWidth, height: horizontalButtonHeight, alignment: .center)
40 | }
41 | }
42 |
43 | if let buttonImageName {
44 | Image(systemName: buttonImageName)
45 | .frame(width: 50, height: 50, alignment: .center)
46 | }
47 | }
48 | .foregroundColor(isSelectedMode ? .yellow : .white)
49 | .overlay {
50 | PrimaryContentBorderView(isSelected: isSelectedMode)
51 | }
52 | }
53 | }
54 |
55 | init(mode: HeaterHorizontalMode,
56 | selectedMode: HeaterHorizontalMode,
57 | buttonTitle: String? = nil,
58 | buttonImageName: String? = nil,
59 | horizontalModeSelectedCallback: @escaping HorizontalModeClosure) {
60 | self.mode = mode
61 | self.selectedMode = selectedMode
62 | self.buttonTitle = buttonTitle
63 | self.buttonImageName = buttonImageName
64 | self.horizontalModeSelectedCallback = horizontalModeSelectedCallback
65 | }
66 | }
67 |
68 | #Preview {
69 | HorizontalButtonView(mode: .auto,
70 | selectedMode: .auto,
71 | buttonTitle: "Vardagsrummet",
72 | buttonImageName: nil,
73 | horizontalModeSelectedCallback: { _ in })
74 | }
75 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Heater/HorizontalModeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HorizontalModeView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-06-22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct HorizontalModeView: View {
11 | let mode: HeaterHorizontalMode
12 | let leftVaneTitle: String
13 | let rightVaneTitle: String
14 | var horizontalModeSelectedCallback: HorizontalModeClosure
15 |
16 | var body: some View {
17 | VStack {
18 | HStack {
19 | HorizontalButtonView(mode: .oneLeft,
20 | selectedMode: mode,
21 | buttonTitle: leftVaneTitle,
22 | horizontalModeSelectedCallback: horizontalModeSelectedCallback)
23 | HorizontalButtonView(mode: .two,
24 | selectedMode: mode,
25 | buttonImageName: "arrow.down.backward",
26 | horizontalModeSelectedCallback: horizontalModeSelectedCallback)
27 | HorizontalButtonView(mode: .three,
28 | selectedMode: mode,
29 | buttonImageName: "arrow.down",
30 | horizontalModeSelectedCallback: horizontalModeSelectedCallback)
31 | HorizontalButtonView(mode: .four,
32 | selectedMode: mode,
33 | buttonImageName: "arrow.down.forward",
34 | horizontalModeSelectedCallback: horizontalModeSelectedCallback)
35 | HorizontalButtonView(mode: .fiveRight,
36 | selectedMode: mode,
37 | buttonTitle: rightVaneTitle,
38 | horizontalModeSelectedCallback: horizontalModeSelectedCallback)
39 | }
40 | .padding([.top, .leading, .trailing])
41 | HStack {
42 | HorizontalButtonView(mode: .auto,
43 | selectedMode: mode,
44 | buttonTitle: "Auto",
45 | horizontalModeSelectedCallback: horizontalModeSelectedCallback)
46 | HorizontalButtonView(mode: .split,
47 | selectedMode: mode,
48 | buttonTitle: "Split",
49 | horizontalModeSelectedCallback: horizontalModeSelectedCallback)
50 | HorizontalButtonView(mode: .swing,
51 | selectedMode: mode,
52 | buttonTitle: "Swing",
53 | horizontalModeSelectedCallback: horizontalModeSelectedCallback)
54 | }
55 | .padding(.bottom)
56 | }
57 | }
58 | }
59 |
60 | struct HorizontalModeView_Previews: PreviewProvider {
61 | static var previews: some View {
62 | HorizontalModeView(mode: .swing, leftVaneTitle: "Left", rightVaneTitle: "Right",
63 | horizontalModeSelectedCallback: { _ in })
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Heater/HvacModeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HvacModeView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-02-19.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct HvacModeView: View {
11 | var heater: HeaterEntity
12 | let mode: HvacMode
13 | let imageSize: CGFloat = 20
14 | var hvacModeSelectedCallback: HeaterStringClosure
15 |
16 | var body: some View {
17 | HStack {
18 | Button {
19 | hvacModeSelectedCallback(heater, .off)
20 | } label: {
21 | HvacButtonLabel(hvacButton: AnyView(
22 | VStack {
23 | Image(systemName: "bolt.slash.fill").resizable()
24 | .frame(width: imageSize, height: imageSize, alignment: .center)
25 | Text("Av")
26 | }
27 | ), isSelectedMode: mode == .off)
28 | }
29 | Button {
30 | hvacModeSelectedCallback(heater, .heat)
31 | } label: {
32 | HvacButtonLabel(hvacButton: AnyView(
33 | VStack {
34 | Image(systemName: "flame").resizable()
35 | .frame(width: imageSize, height: imageSize, alignment: .center)
36 | Text("Värme")
37 | }
38 | ), isSelectedMode: mode == .heat)
39 | }
40 | Button {
41 | hvacModeSelectedCallback(heater, .cool)
42 | } label: {
43 | HvacButtonLabel(hvacButton: AnyView(
44 | VStack {
45 | Image(systemName: "snowflake").resizable()
46 | .frame(width: imageSize, height: imageSize, alignment: .center)
47 | Text("Kyla")
48 | }
49 | ), isSelectedMode: mode == .cool)
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Heater/NumberTextView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NumberTextView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-06-22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NumberTextView: View {
11 | var pickerTextWidth: CGFloat
12 | @Binding var targetTemperature: Double
13 | @Binding var selectedNewTarget: Bool
14 | var index: Double
15 | let numberPickerFormat: NumberFormatter
16 | var body: some View {
17 | Text("\(index as NSNumber, formatter: numberPickerFormat)")
18 | .id(index)
19 | .font(targetTemperature == index ? .title : .body)
20 | .frame(width: pickerTextWidth, height: 30)
21 | .gesture(TapGesture().onEnded {
22 | selectedNewTarget = true
23 | targetTemperature = index
24 | })
25 | .foregroundColor(targetTemperature == index ? .white : .white.opacity(0.667))
26 | }
27 | }
28 |
29 | struct NumberTextView_Previews: PreviewProvider {
30 | static var previews: some View {
31 | NumberTextView(pickerTextWidth: 20, targetTemperature: .constant(22), selectedNewTarget: .constant(false),
32 | index: 10, numberPickerFormat: NumberFormatter())
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Heater/ThermometerGroupView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThermometerGroupView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-06-22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ThermometerGroupView: View {
11 | var therm1: Entity
12 | var therm2: Entity
13 | var therm3: Entity
14 | var therm4: Entity
15 |
16 | var body: some View {
17 | HStack {
18 | ThermometerView(thermometer: therm1)
19 | ThermometerView(thermometer: therm2)
20 | .padding(.leading)
21 | ThermometerView(thermometer: therm3)
22 | .padding(.horizontal)
23 | ThermometerView(thermometer: therm4)
24 | }
25 | }
26 | }
27 |
28 | struct ThermometerGroupView_Previews: PreviewProvider {
29 | static var previews: some View {
30 | let therm = Entity(entityId: EntityId.thermGym)
31 | ThermometerGroupView(therm1: therm, therm2: therm, therm3: therm, therm4: therm)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Heater/ThermometerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThermometerView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-06-22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ThermometerView: View {
11 | var thermometer: Entity
12 | var body: some View {
13 | VStack {
14 | Text(thermometer.entityId.roomTitle)
15 | .font(.caption2)
16 | .foregroundColor(.white)
17 | .lineLimit(1)
18 | .minimumScaleFactor(0.2)
19 | Text(thermometer.state == "unavailable" ? "?" :
20 | "\(thermometer.state.replacingOccurrences(of: ".", with: ",")) ℃")
21 | .foregroundColor(.white)
22 | .lineLimit(1)
23 | .minimumScaleFactor(0.01)
24 | }
25 | }
26 | }
27 |
28 | private extension EntityId {
29 | var roomTitle: String {
30 | switch self {
31 | case .thermCorridor:
32 | "Korridoren"
33 | case .thermBedroom:
34 | "Sovrummet"
35 | case .thermVince:
36 | "Vince rum"
37 | case .thermGym:
38 | "Gymmet"
39 | case .thermPlayroom:
40 | "Lekrummet"
41 | case .thermCommonarea:
42 | "Vardagsrummet"
43 | case .thermGuest:
44 | "Gästrummet"
45 | case .thermKitchen:
46 | "Köket"
47 | default:
48 | "Missing room"
49 | }
50 | }
51 | }
52 |
53 | struct ThermometerView_Previews: PreviewProvider {
54 | static var previews: some View {
55 | let therm = Entity(entityId: EntityId.thermGym)
56 | ThermometerView(thermometer: therm)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Heater/VerticalButtonView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VerticalButtonView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-11-10.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct VerticalButtonView: View {
11 | let verticalButtonWidth: CGFloat = 50
12 | let verticalButtonHeight: CGFloat = 50
13 | let verticalButtonCornerRadius: CGFloat = 10
14 | var isSelectedMode: Bool {
15 | mode == selectedMode
16 | }
17 |
18 | let mode: HeaterVerticalMode
19 | let selectedMode: HeaterVerticalMode
20 | var verticalModeSelectedCallback: VerticalModeClosure
21 |
22 | var body: some View {
23 | Button {
24 | verticalModeSelectedCallback(mode)
25 | } label: {
26 | Group {
27 | if let buttonTitle = mode.buttonTitle {
28 | if buttonTitle.count > 1 {
29 | Text(buttonTitle).font(.body)
30 | .frame(width: verticalButtonWidth, height: verticalButtonHeight, alignment: .center)
31 | .padding(.horizontal, 5)
32 | } else {
33 | Text(buttonTitle).font(.title)
34 | .frame(width: verticalButtonWidth, height: verticalButtonHeight, alignment: .center)
35 | }
36 | }
37 |
38 | if let buttonImageName = mode.buttonImageName {
39 | Image(systemName: buttonImageName)
40 | .frame(width: 50, height: 50, alignment: .center)
41 | }
42 | }
43 | .foregroundColor(isSelectedMode ? .yellow : .white)
44 | .overlay {
45 | PrimaryContentBorderView(isSelected: isSelectedMode)
46 | }
47 | }
48 | }
49 | }
50 |
51 | #Preview {
52 | VerticalButtonView(mode: .auto, selectedMode: .highest, verticalModeSelectedCallback: { _ in })
53 | }
54 |
55 | private extension HeaterVerticalMode {
56 | var buttonTitle: String? {
57 | switch self {
58 | case .auto:
59 | "Auto"
60 | case .highest:
61 | "Upp"
62 | case .lowest:
63 | "Ner"
64 | case .swing:
65 | "Swing"
66 | default:
67 | nil
68 | }
69 | }
70 |
71 | var buttonImageName: String? {
72 | switch self {
73 | case .position2:
74 | "arrow.up.forward"
75 | case .position3:
76 | "arrow.right"
77 | case .position4:
78 | "arrow.down.forward"
79 | default:
80 | nil
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Heater/VerticalPositionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VerticalPositionView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-06-22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct VerticalPositionView: View {
11 | let mode: HeaterVerticalMode
12 | var verticalModeSelectedCallback: VerticalModeClosure
13 |
14 | var body: some View {
15 | HStack {
16 | VStack {
17 | VerticalButtonView(mode: .highest, selectedMode: mode, verticalModeSelectedCallback: verticalModeSelectedCallback)
18 | VerticalButtonView(mode: .position2, selectedMode: mode, verticalModeSelectedCallback: verticalModeSelectedCallback)
19 | VerticalButtonView(mode: .position3, selectedMode: mode, verticalModeSelectedCallback: verticalModeSelectedCallback)
20 | VerticalButtonView(mode: .position4, selectedMode: mode, verticalModeSelectedCallback: verticalModeSelectedCallback)
21 | VerticalButtonView(mode: .lowest, selectedMode: mode, verticalModeSelectedCallback: verticalModeSelectedCallback)
22 | }
23 | .padding([.top, .leading, .trailing])
24 | VStack {
25 | VerticalButtonView(mode: .auto, selectedMode: mode, verticalModeSelectedCallback: verticalModeSelectedCallback)
26 | VerticalButtonView(mode: .swing, selectedMode: mode, verticalModeSelectedCallback: verticalModeSelectedCallback)
27 | }
28 | .padding(.bottom)
29 | }
30 | }
31 | }
32 |
33 | struct HeaterVerticalPositionView_Previews: PreviewProvider {
34 | static var previews: some View {
35 | VerticalPositionView(mode: .lowest, verticalModeSelectedCallback: { _ in })
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Home/CoffeeMachineSchedulingView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CoffeeMachineSchedulingView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-10-19.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct CoffeeMachineSchedulingView: View {
11 | private var dateProxy: Binding {
12 | Binding(get: {
13 | coffeeMachineStartTime.date
14 | }, set: {
15 | coffeeMachineStartTime.date = $0
16 | setCoffeeMachineStartTime(coffeeMachineStartTime)
17 | })
18 | }
19 |
20 | @Binding var isVisible: Bool
21 | let title: String
22 | @Binding var coffeeMachineStartTime: Entity
23 | var coffeeMachineStartTimeEnabled: Entity
24 | let setCoffeeMachineStartTime: EntityClosure
25 | let toggleStartTimeEnabledAction: VoidClosure
26 |
27 | var body: some View {
28 | ZStack {
29 | FullScreenBackgroundOverlay()
30 | .onTapGesture {
31 | isVisible = false
32 | }
33 | RoundedRectangle(cornerRadius: 20)
34 | .foregroundColor(.black)
35 | .frame(width: 220, height: 150)
36 | VStack(spacing: 16) {
37 | Text(title)
38 | .font(.title2)
39 | .foregroundStyle(.white)
40 | HStack(spacing: 16) {
41 | DatePicker("", selection: dateProxy, displayedComponents: .hourAndMinute)
42 | .labelsHidden()
43 | .colorScheme(.dark)
44 | Button {
45 | toggleStartTimeEnabledAction()
46 | } label: {
47 | Image(systemImageName: .clock)
48 | .resizable()
49 | .frame(width: 40, height: 40)
50 | .foregroundColor(coffeeMachineStartTimeEnabled.isActive ? .lightBlue : .white)
51 | }
52 | }
53 | }
54 | }
55 | .frame(maxWidth: .infinity, maxHeight: .infinity)
56 | }
57 | }
58 |
59 | #Preview {
60 | CoffeeMachineSchedulingView(isVisible: .constant(true),
61 | title: "Kaffemaskinen",
62 | coffeeMachineStartTime: .constant(.init(entityId: .coffeeMachineStartTime)),
63 | coffeeMachineStartTimeEnabled: .init(entityId: .coffeeMachineStartTimeEnabled),
64 | setCoffeeMachineStartTime: { _ in },
65 | toggleStartTimeEnabledAction: {})
66 | }
67 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Labels/HeaterGroupLabels.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HeaterGroupLabels.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-02-19.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct HvacButtonLabel: View {
11 | var hvacButton: AnyView
12 | var isSelectedMode: Bool
13 | let hvacButtonSize: CGFloat = 60
14 | let hvacButtonCornerRadius: CGFloat = 15
15 | var body: some View {
16 | Group {
17 | hvacButton
18 | }
19 | .frame(width: hvacButtonSize, height: hvacButtonSize, alignment: .center)
20 | .foregroundColor(isSelectedMode ? .yellow : .white)
21 | .font(isSelectedMode ? .buttonFontLarge.bold() : .buttonFontMedium)
22 | .overlay {
23 | PrimaryContentBorderView(isSelected: isSelectedMode)
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Light/BulbButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BulbButton.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-08-18.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct BulbButton: View {
11 | private let buttonImageSIze: CGFloat = 24
12 |
13 | var light: LightEntity
14 | let onTapAction: AsyncLightClosure
15 |
16 | var body: some View {
17 | Button {
18 | Task { @MainActor in
19 | await onTapAction(light)
20 | }
21 | } label: {
22 | if light.isActive {
23 | Image(systemName: "lightbulb.fill")
24 | .font(.system(size: buttonImageSIze))
25 | .foregroundColor(.yellow)
26 | } else {
27 | Image(systemName: "lightbulb.slash")
28 | .font(.system(size: buttonImageSIze))
29 | }
30 | }
31 | .padding(.vertical, 3.0)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Light/DualBulbRoomView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DualBulbRoomView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-07-07.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct DualBulbRoomView: View {
11 | let roomName: String
12 | @Binding var lightGroup: LightEntity
13 | @Binding var light1: LightEntity
14 | @Binding var light2: LightEntity
15 | let light1Name: String
16 | let light2Name: String
17 | let onTapAction: AsyncSlideableClosure
18 | let onSliderChangeAction: SlideableIntClosure
19 | let onSliderReleaseAction: AsyncSlideableClosure
20 | let sliderWidth: CGFloat
21 | let sliderHeight: CGFloat
22 | let bulbTitleSize: CGFloat
23 | let roomTitleSize: CGFloat
24 |
25 | var body: some View {
26 | VStack {
27 | VStack {
28 | Text(roomName)
29 | .font(.system(size: roomTitleSize))
30 | .foregroundColor(.white)
31 | BulbButton(light: lightGroup, onTapAction: onTapAction)
32 | }
33 |
34 | HStack {
35 | VStack {
36 | VerticalSlider(slideable: light1,
37 | onSliderChangeAction: onSliderChangeAction,
38 | onSliderReleaseAction: onSliderReleaseAction,
39 | onTapAction: onTapAction)
40 | .frame(width: sliderWidth, height: sliderHeight, alignment: .center)
41 | Text(light1Name)
42 | .font(.system(size: bulbTitleSize))
43 | .foregroundColor(.white)
44 | }
45 |
46 | VStack {
47 | VerticalSlider(slideable: light2,
48 | onSliderChangeAction: onSliderChangeAction,
49 | onSliderReleaseAction: onSliderReleaseAction,
50 | onTapAction: onTapAction)
51 | .frame(width: sliderWidth, height: sliderHeight, alignment: .center)
52 | Text(light2Name)
53 | .font(.system(size: bulbTitleSize))
54 | .foregroundColor(.white)
55 | }
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Light/SingleRoomLight.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SingleRoomLight.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-07-13.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SingleRoomLight: View {
11 | let roomName: String
12 | @Binding var light: LightEntity
13 | let onTapAction: AsyncSlideableClosure
14 | let onSliderChangeAction: SlideableIntClosure
15 | let onSliderReleaseAction: AsyncSlideableClosure
16 | let roomTitleSize: CGFloat
17 | let sliderWidth: CGFloat
18 | let sliderHeight: CGFloat
19 |
20 | var body: some View {
21 | VStack {
22 | VStack {
23 | Text(roomName)
24 | .font(.system(size: roomTitleSize))
25 | .foregroundColor(.white)
26 | BulbButton(light: light, onTapAction: onTapAction)
27 | }
28 |
29 | HStack {
30 | VerticalSlider(slideable: light,
31 | onSliderChangeAction: onSliderChangeAction,
32 | onSliderReleaseAction: onSliderReleaseAction,
33 | onTapAction: onTapAction)
34 | .frame(width: sliderWidth, height: sliderHeight, alignment: .center)
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Lynk/DateTimePickerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DateTimePickerView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-06-14.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct DateTimePickerView: View {
11 | let title: String
12 | let displayComponents: DatePickerComponents
13 | @Binding var dateTime: Entity
14 | @Binding var dateTimeEnabled: Entity
15 | @State var dateTimeLoaded = false
16 | let updateToggle: AsyncEntityClosure
17 | let setDateTimeClosure: EntityClosure
18 |
19 | var body: some View {
20 | HStack {
21 | Text(title)
22 | Spacer()
23 | .frame(width: 20)
24 | DatePicker("", selection: $dateTime.date, displayedComponents: displayComponents)
25 | .labelsHidden()
26 | .onChange(of: dateTime.date) {
27 | if dateTimeLoaded {
28 | dateTimeEnabled.isActive = true
29 | setDateTimeClosure(dateTime)
30 | } else {
31 | dateTimeLoaded = true
32 | }
33 | }
34 | Toggle("", isOn: $dateTimeEnabled.isActive)
35 | .onChange(of: dateTimeEnabled.isActive) {
36 | Task {
37 | await updateToggle(dateTimeEnabled)
38 | }
39 | }
40 | }
41 | .padding([.horizontal, .bottom], 8)
42 | }
43 | }
44 |
45 | struct DatePickerView_Previews: PreviewProvider {
46 | static var previews: some View {
47 | DateTimePickerView(title: "Title",
48 | displayComponents: .hourAndMinute,
49 | dateTime: .constant(.init(entityId: .heaterCorridor)),
50 | dateTimeEnabled: .constant(.init(entityId: .heaterCorridor)),
51 | updateToggle: { _ in },
52 | setDateTimeClosure: { _ in })
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Lynk/LimitPickerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LimitPickerView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2023-02-24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct LimitPickerView: View {
11 | let limitEntity: InputNumberEntity
12 | let saveChargerLimit: EntityIdDoubleClosure
13 | @State var currentLimit: Double
14 | let pickerTextWidth: CGFloat = 60
15 | var body: some View {
16 | ZStack {
17 | FullScreenBackgroundOverlay()
18 | .onTapGesture {
19 | saveChargerLimit(limitEntity.entityId, currentLimit)
20 | }
21 | RoundedRectangle(cornerRadius: 40)
22 | .foregroundColor(.black)
23 | .opacity(0.7)
24 | .frame(width: pickerTextWidth * 4,
25 | height: 160)
26 | .onTapGesture {
27 | saveChargerLimit(limitEntity.entityId, currentLimit)
28 | }
29 | VStack {
30 | NumberPickerScrollView(entityId: limitEntity.entityId,
31 | targetNumber: $currentLimit,
32 | numberSelectedCallback: saveChargerLimit,
33 | pickerTextWidth: pickerTextWidth,
34 | strideFrom: 50,
35 | strideTo: 101,
36 | strideStep: 10)
37 | }
38 | }
39 | }
40 | }
41 |
42 | struct LimitPickerView_Previews: PreviewProvider {
43 | static var previews: some View {
44 | LimitPickerView(limitEntity: .init(entityId: .lynkDoorLock), saveChargerLimit: { _, _ in }, currentLimit: 50)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Roborock/RoborockInfoView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoborockInfoView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-06-22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct RoborockInfoView: View {
11 | var lastCleanArea: String
12 | var batteryLevel: Int
13 | @Binding var showingMapView: Bool
14 |
15 | var body: some View {
16 | HStack(alignment: .center) {
17 | VStack(alignment: .leading) {
18 | Text("Senast städat: \(lastCleanArea.components(separatedBy: ".")[0]) m²")
19 | .foregroundColor(.white)
20 | Text("Batteri-nivå: \(batteryLevel)%")
21 | .foregroundColor(.white)
22 | }
23 | Button(action: {
24 | showingMapView = true
25 | }, label: {
26 | Image(systemName: "map.circle")
27 | .resizable(resizingMode: .stretch)
28 | .frame(width: 35,
29 | height: 35)
30 | .foregroundColor(.white)
31 | })
32 | .padding(.leading)
33 | }
34 | }
35 | }
36 |
37 | struct RoborockInfo_Previews: PreviewProvider {
38 | static var previews: some View {
39 | RoborockInfoView(
40 | lastCleanArea: "",
41 | batteryLevel: 3,
42 | showingMapView: .constant(false)
43 | )
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Roborock/RoborockMainButtons.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoborockMainButtons.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-06-22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct RoborockMainButtons: View {
11 | var roborock: RoborockEntity
12 | let toggleCleaningClosure: MainActorVoidClosure
13 | let dockRoborockClosure: MainActorVoidClosure
14 | let sendRoborockToBinClosure: MainActorVoidClosure
15 | let locateRoborockClosure: MainActorVoidClosure
16 | let manualEmptyClosure: MainActorVoidClosure
17 |
18 | var body: some View {
19 | HStack {
20 | Group {
21 | ServiceButtonView(buttonTitle: roborock.cleanButtonTitle,
22 | isActive: roborock.isCleaning,
23 | icon: roborock.cleanIcon,
24 | iconHeight: 25,
25 | action: toggleCleaningClosure)
26 | ServiceButtonView(buttonTitle: roborock.returnButtonTitle,
27 | isActive: roborock.isReturning,
28 | icon: roborock.returningIcon,
29 | imageSize: 25,
30 | action: dockRoborockClosure)
31 | ServiceButtonView(buttonTitle: "Hitta",
32 | icon: .init(systemImageName: .scope),
33 | imageSize: 25,
34 | action: locateRoborockClosure)
35 | ServiceButtonView(buttonTitle: "Töm",
36 | icon: .init(systemImageName: .trash),
37 | imageSize: 25,
38 | action: sendRoborockToBinClosure)
39 | .contextMenu {
40 | Button(action: manualEmptyClosure, label: {
41 | Text("Manuell tömning")
42 | })
43 | }
44 | }
45 | }
46 | }
47 | }
48 |
49 | struct RoborockMainButtons_Previews: PreviewProvider {
50 | static var previews: some View {
51 | RoborockMainButtons(
52 | roborock: .init(entityId: .roborock, state: ""),
53 | toggleCleaningClosure: {},
54 | dockRoborockClosure: {},
55 | sendRoborockToBinClosure: {},
56 | locateRoborockClosure: {},
57 | manualEmptyClosure: {}
58 | )
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Roborock/RoborockMapImageView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoborockMapImageView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-06-03.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct RoborockMapImageView: View {
11 | @ObservedObject var viewModel: RoborockViewModel
12 |
13 | var body: some View {
14 | ZStack {
15 | FullScreenBackgroundOverlay()
16 | .onTapGesture {
17 | viewModel.isShowingMapView = false
18 | }
19 | if let url = URL(string: viewModel.imagageURLString) {
20 | AsyncImage(url: url) { phase in
21 | if let image = phase.image {
22 | ZoomableImageView(image: image, isPortrait: true, isFullScreen: .constant(false), initialScale: 1.5)
23 | .frame(width: 300, height: 330)
24 | } else if phase.error != nil {
25 | Image(systemName: "exclamationmark.triangle")
26 | .foregroundStyle(.red)
27 | .frame(width: 100, height: 100)
28 | .padding()
29 | } else {
30 | ProgressView()
31 | .frame(width: 100, height: 100)
32 | .padding()
33 | }
34 | }
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/IntelliNest/Views/Roborock/SinceEmptiedView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SinceEmptiedView.swift
3 | // IntelliNest
4 | //
5 | // Created by Tobias on 2022-06-22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SinceEmptiedView: View {
11 | var emptiedAtDate: String
12 | var areaSinceEmpty: String
13 |
14 | var body: some View {
15 | HStack(alignment: .top) {
16 | Text("Senaste tömningen: ")
17 | .foregroundColor(.white)
18 | VStack(alignment: .trailing) {
19 | Text("\(emptiedAtDate.components(separatedBy: " ")[0])")
20 | .foregroundColor(.white)
21 | Text("\(areaSinceEmpty.components(separatedBy: ".")[0]) m²")
22 | .foregroundColor(.white)
23 | }
24 | Spacer()
25 | }
26 | }
27 | }
28 |
29 | struct SinceEmptied_Previews: PreviewProvider {
30 | static var previews: some View {
31 | SinceEmptiedView(emptiedAtDate: "", areaSinceEmpty: "")
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/IntelliNestTests/InputNumberEntityTest.swift:
--------------------------------------------------------------------------------
1 | @testable import IntelliNest
2 | import XCTest
3 |
4 | class InputNumberEntityTests: XCTestCase {
5 | func testInit() {
6 | let entity = InputNumberEntity(entityId: .thermKitchen)
7 |
8 | XCTAssertEqual(entity.entityId, .thermKitchen)
9 | XCTAssertEqual(entity.state, "Loading")
10 | XCTAssertEqual(entity.lastChanged, .distantPast)
11 | XCTAssertEqual(entity.lastUpdated, .distantPast)
12 | XCTAssertEqual(entity.isActive, false)
13 | XCTAssertEqual(entity.inputNumber, 0)
14 | }
15 |
16 | func testInitFromDecoder() throws {
17 | let json = Data("""
18 | {
19 | "entity_id": "input_number.kia_climate_temperature",
20 | "state": "21.0",
21 | "attributes": {
22 | "initial": null,
23 | "editable": true,
24 | "min": 16.0,
25 | "max": 26.0,
26 | "step": 0.5,
27 | "mode": "slider",
28 | "icon": "mdi:thermometer",
29 | "friendly_name": "Kia climate temperature"
30 | },
31 | "last_changed": "2023-02-24T05:40:30.846631+00:00",
32 | "last_updated": "2023-02-24T05:40:30.846631+00:00",
33 | "context": {
34 | "id": "01GT0YZVZY8XCG69BJ0HE9VAN1",
35 | "parent_id": null,
36 | "user_id": null
37 | }
38 | }
39 | """.utf8)
40 | let decoder = JSONDecoder()
41 | let date = try XCTUnwrap(Date.fromISO8601("2023-02-24T05:40:30.846631+00:00"))
42 | let entity = try decoder.decode(InputNumberEntity.self, from: json)
43 |
44 | XCTAssertEqual(entity.state, "21.0")
45 | XCTAssertEqual(entity.lastChanged, date)
46 | XCTAssertEqual(entity.lastUpdated, date)
47 | XCTAssertEqual(entity.isActive, false)
48 | XCTAssertEqual(entity.inputNumber, 21.0)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/IntelliNestTests/LightEntityTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LightEntityTests.swift
3 | // IntelliNestTests
4 | //
5 | // Created by Tobias on 2023-06-17.
6 | //
7 |
8 | @testable import IntelliNest
9 | import XCTest
10 |
11 | final class LightEntityTests: XCTestCase {
12 | var light: LightEntity!
13 |
14 | override func setUpWithError() throws {
15 | try? super.setUpWithError()
16 |
17 | light = LightEntity(entityId: .sofa, state: "off")
18 | light.brightness = 103
19 | }
20 |
21 | override func tearDownWithError() throws {
22 | light = nil
23 | try super.tearDownWithError()
24 | }
25 |
26 | func testDecode() throws {
27 | XCTAssertEqual(light.entityId, .sofa, "Failed to decode 'entityId'")
28 | XCTAssertEqual(light.state, "off", "Failed to decode 'state'")
29 | XCTAssertEqual(light.brightness, 103, "Failed to decode 'brightness'")
30 | XCTAssertEqual(light.isActive, false, "Failed to decode 'isActive'")
31 | XCTAssertNotNil(light.nextUpdate, "Failed to decode 'nextUpdate'")
32 | }
33 |
34 | func testUpdateIsActive() {
35 | light.updateIsActive()
36 | XCTAssertEqual(light.isActive, false, "Failed to update 'isActive'")
37 | }
38 |
39 | func testSetNextUpdateTime() {
40 | light.setNextUpdateTime()
41 | XCTAssertNotNil(light.nextUpdate, "Failed to set 'nextUpdate'")
42 | }
43 |
44 | func testEquality() throws {
45 | let copy = light
46 | XCTAssertEqual(light, copy, "Failed to test equality")
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/IntelliNestTests/LockEntityTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LockEntityTest.swift
3 | // IntelliNestTests
4 | //
5 | // Created by Tobias on 2022-05-23.
6 | //
7 |
8 | import XCTest
9 |
10 | @testable import IntelliNest
11 | class LockEntityTest: XCTestCase {
12 | override func setUpWithError() throws {
13 | // Put setup code here. This method is called before the invocation of each test method in the class.
14 | }
15 |
16 | override func tearDownWithError() throws {
17 | // Put teardown code here. This method is called after the invocation of each test method in the class.
18 | }
19 |
20 | func testStateToString() {
21 | // Given
22 | var lockEntity = LockEntity(entityId: EntityId.framdorren, state: "Loading")
23 | lockEntity.state = "unlocked"
24 | let translatedStateUnlocked = lockEntity.stateToString()
25 | lockEntity.state = "locked"
26 | let translatedStateLocked = lockEntity.stateToString()
27 | lockEntity.state = "locking"
28 | let translatedStateLocking = lockEntity.stateToString()
29 | lockEntity.state = "unlocking"
30 | let translatedStateUnlocking = lockEntity.stateToString()
31 |
32 | // When
33 |
34 | // Then
35 | XCTAssertEqual("olåst", translatedStateUnlocked)
36 | XCTAssertEqual("låst", translatedStateLocked)
37 | XCTAssertEqual("låser", translatedStateLocking)
38 | XCTAssertEqual("låser upp", translatedStateUnlocking)
39 | }
40 |
41 | func testActionText() {
42 | // Given
43 | var lockEntity = LockEntity(entityId: EntityId.framdorren, state: "Loading")
44 | lockEntity.state = "unlocked"
45 | let lockEntityUnlocked = lockEntity
46 | lockEntity.state = "locked"
47 | let lockEntityLocked = lockEntity
48 | lockEntity.state = "unlocking"
49 | let lockEntityUnlocking = lockEntity
50 | lockEntity.state = "locking"
51 | let lockEntityLocking = lockEntity
52 |
53 | // When
54 |
55 | // Then
56 | XCTAssertEqual("Lås", lockEntityUnlocked.actionText)
57 | XCTAssertEqual("Lås upp", lockEntityLocked.actionText)
58 | XCTAssertEqual("Låser upp", lockEntityUnlocking.actionText)
59 | XCTAssertEqual("Låser", lockEntityLocking.actionText)
60 | }
61 |
62 | func testCalculateIsActive() {
63 | // Given
64 | var lockEntity = LockEntity(entityId: EntityId.framdorren, state: "Loading")
65 | lockEntity.state = "unlocked"
66 | let lockEntityUnlocked = lockEntity
67 | lockEntity.state = "locked"
68 | let lockEntityLocked = lockEntity
69 | lockEntity.state = "unlocking"
70 | let lockEntityUnlocking = lockEntity
71 | lockEntity.state = "locking"
72 | let lockEntityLocking = lockEntity
73 | // When
74 | // ...
75 |
76 | // Then
77 | XCTAssertTrue(lockEntityUnlocked.isActive)
78 | XCTAssertFalse(lockEntityLocked.isActive)
79 | XCTAssertTrue(lockEntityUnlocking.isActive)
80 | XCTAssertTrue(lockEntityLocking.isActive)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/IntelliNestTests/URLProtocolStub.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLProtocolStub.swift
3 | // IntelliNestTests
4 | //
5 | // Created by Tobias on 2023-05-06.
6 | //
7 |
8 | import Foundation
9 |
10 | class URLProtocolStub: URLProtocol {
11 | private static var stubs: [URL: Stub] = [:]
12 | private static var requestObserver: ((URLRequest) -> Void)?
13 |
14 | private struct Stub {
15 | let data: Data?
16 | let response: URLResponse?
17 | let error: Error?
18 | }
19 |
20 | static func setStub(for url: URL, data: Data?, response: URLResponse?, error: Error?) {
21 | let stub = Stub(data: data, response: response, error: error)
22 | stubs[url] = stub
23 | }
24 |
25 | static func observerRequests(observer: @escaping (URLRequest) -> Void) {
26 | requestObserver = observer
27 | }
28 |
29 | static func startInterceptingRequests() {
30 | URLProtocolStub.registerClass(URLProtocolStub.self)
31 | }
32 |
33 | static func stopInterceptingRequests() {
34 | URLProtocolStub.unregisterClass(URLProtocolStub.self)
35 | stubs.removeAll()
36 | requestObserver = nil
37 | }
38 |
39 | override class func canInit(with request: URLRequest) -> Bool {
40 | requestObserver?(request)
41 | return true
42 | }
43 |
44 | override class func canonicalRequest(for request: URLRequest) -> URLRequest {
45 | request
46 | }
47 |
48 | override func startLoading() {
49 | if let url = request.url, let stub = URLProtocolStub.stubs[url] {
50 | if let data = stub.data {
51 | client?.urlProtocol(self, didLoad: data)
52 | }
53 |
54 | if let response = stub.response {
55 | client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
56 | }
57 |
58 | if let error = stub.error {
59 | client?.urlProtocol(self, didFailWithError: error)
60 | }
61 | }
62 | client?.urlProtocolDidFinishLoading(self)
63 | }
64 |
65 | override func stopLoading() {}
66 |
67 | static func createStubbedURLSession() -> URLSession {
68 | let configuration = URLSessionConfiguration.ephemeral
69 | configuration.protocolClasses = [URLProtocolStub.self]
70 | let urlSession = URLSession(configuration: configuration)
71 | return urlSession
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/IntelliWidget/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/IntelliWidget/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/IntelliWidget/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/IntelliWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/IntelliWidget/Assets.xcassets/widget-home-icon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "widget-home-icon.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | },
21 | "properties" : {
22 | "preserves-vector-representation" : true
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/IntelliWidget/Assets.xcassets/widget-home-icon.imageset/widget-home-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TobiasLaross/IntelliNest/627123f9c34778d8c69efbb70655666d8bfc6075/IntelliWidget/Assets.xcassets/widget-home-icon.imageset/widget-home-icon.png
--------------------------------------------------------------------------------
/IntelliWidget/CarHeaterWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CarHeaterWidget.swift
3 | // IntelliWidget
4 | //
5 | // Created by Tobias on 2024-01-12.
6 | //
7 |
8 | import SwiftUI
9 | import WidgetKit
10 |
11 | struct CarHeaterEntryView: View {
12 | var entry: SimpleEntry
13 | let frameSize = 55.0
14 |
15 | var body: some View {
16 | ZStack {
17 | AccessoryWidgetBackground()
18 | Link(destination: URL(string: "IntelliNest://start-car-heater")!) {
19 | Image(systemName: "car")
20 | .resizable()
21 | .aspectRatio(contentMode: .fit)
22 | .padding(12)
23 | }
24 | }
25 | }
26 | }
27 |
28 | @MainActor
29 | struct Provider: @preconcurrency TimelineProvider {
30 | @preconcurrency func placeholder(in context: Context) -> SimpleEntry {
31 | SimpleEntry(date: Date(), isSarahsPillsTaken: false)
32 | }
33 |
34 | @preconcurrency func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> Void) {
35 | let entry = createEntry()
36 | completion(entry)
37 | }
38 |
39 | @preconcurrency func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
40 | let entry = createEntry()
41 |
42 | var calendar = Calendar.current
43 | calendar.timeZone = TimeZone.current
44 | let now = Date()
45 | guard let nextMidnight = calendar.nextDate(after: now,
46 | matching: DateComponents(hour: 0, minute: 0, second: 0),
47 | matchingPolicy: .nextTime) else {
48 | let timeline = Timeline(entries: [entry], policy: .after(now.addingTimeInterval(86400)))
49 | completion(timeline)
50 | return
51 | }
52 |
53 | let midnightEntry = SimpleEntry(date: nextMidnight, isSarahsPillsTaken: false)
54 | let timeline = Timeline(entries: [entry, midnightEntry], policy: .after(nextMidnight))
55 | completion(timeline)
56 | }
57 |
58 | private func createEntry() -> SimpleEntry {
59 | let lastTakenPillsDate = UserDefaults.shared.value(forKey: StorageKeys.sarahPills.rawValue) as? Date
60 | let isSarahsPillsTaken = Calendar.current.isDateInToday(lastTakenPillsDate ?? .distantPast)
61 | return SimpleEntry(date: Date(), isSarahsPillsTaken: isSarahsPillsTaken)
62 | }
63 | }
64 |
65 | struct SimpleEntry: TimelineEntry {
66 | let date: Date
67 | let isSarahsPillsTaken: Bool
68 | }
69 |
70 | struct CarHeaterWidget: Widget {
71 | let kind: String = "CarHeaterWidget"
72 |
73 | var body: some WidgetConfiguration {
74 | StaticConfiguration(kind: kind, provider: Provider()) { entry in
75 | CarHeaterEntryView(entry: entry)
76 | .containerBackground(.fill.tertiary, for: .widget)
77 | }
78 | .configurationDisplayName("IntelliWidget")
79 | .description("Tap the car to activate car heater")
80 | .supportedFamilies([.accessoryCircular])
81 | }
82 | }
83 |
84 | struct IntelliWidget_Previews: PreviewProvider {
85 | static var previews: some View {
86 | CarHeaterEntryView(entry: SimpleEntry(date: Date(), isSarahsPillsTaken: false))
87 | .containerBackground(.fill.tertiary, for: .widget)
88 | .previewContext(WidgetPreviewContext(family: .accessoryRectangular))
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/IntelliWidget/HomeWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeWidget.swift
3 | // IntelliWidgetExtension
4 | //
5 | // Created by Tobias on 2024-01-20.
6 | //
7 |
8 | import SwiftUI
9 | import WidgetKit
10 |
11 | struct HomeWidgetEntryView: View {
12 | var entry: SimpleEntry
13 |
14 | var body: some View {
15 | ZStack {
16 | AccessoryWidgetBackground()
17 | Link(destination: URL(string: "IntelliNest://home")!) {
18 | if UserManager.currentUser == .sarah && !entry.isSarahsPillsTaken {
19 | Image(systemName: "pills.fill")
20 | .resizable()
21 | .aspectRatio(contentMode: .fit)
22 | .padding(10)
23 | } else {
24 | // Image("widget-home-icon")
25 | Image(systemName: "house")
26 | .resizable()
27 | .aspectRatio(contentMode: .fit)
28 | .padding(12)
29 | }
30 | }
31 | }
32 | }
33 | }
34 |
35 | struct HomeWidget: Widget {
36 | let kind: String = "HomeWidget"
37 |
38 | var body: some WidgetConfiguration {
39 | StaticConfiguration(kind: kind, provider: Provider()) { entry in
40 | HomeWidgetEntryView(entry: entry)
41 | .containerBackground(.fill.tertiary, for: .widget)
42 | }
43 | .configurationDisplayName("IntelliWidget")
44 | .description("Tap the house to navigate to home screen")
45 | .supportedFamilies([.accessoryCircular])
46 | }
47 | }
48 |
49 | #Preview {
50 | HomeWidgetEntryView(entry: .init(date: Date(), isSarahsPillsTaken: true))
51 | }
52 |
--------------------------------------------------------------------------------
/IntelliWidget/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSExtension
6 |
7 | NSExtensionAttributes
8 |
9 | WKWidget
10 |
11 | SupportedFamilies
12 |
13 | accessoryCircular
14 |
15 |
16 |
17 | NSExtensionPointIdentifier
18 | com.apple.widgetkit-extension
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/IntelliWidget/IntelliWidgetBundle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IntelliWidgetBundle.swift
3 | // IntelliWidget
4 | //
5 | // Created by Tobias on 2024-01-12.
6 | //
7 |
8 | import SwiftUI
9 | import WidgetKit
10 |
11 | @main
12 | struct IntelliWidgetBundle: WidgetBundle {
13 | var body: some Widget {
14 | HomeWidget()
15 | CarHeaterWidget()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/IntelliWidget/IntelliWidgetExtension.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.application-groups
6 |
7 | group.se.laross.intellinest.shared
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Tobias Laross
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/buildServer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xcode build server",
3 | "version": "0.2",
4 | "bspVersion": "2.0",
5 | "languages": [
6 | "c",
7 | "cpp",
8 | "objective-c",
9 | "objective-cpp",
10 | "swift"
11 | ],
12 | "argv": [
13 | "/opt/homebrew/bin/xcode-build-server"
14 | ],
15 | "workspace": "/Users/tobias/Developer/personal/IntelliNest/IntelliNest.xcodeproj/project.xcworkspace",
16 | "build_root": "/Users/tobias/Library/Developer/Xcode/DerivedData/IntelliNest-dadbqqzjlyieqohfxijjwikewmvx",
17 | "scheme": "IntelliNest",
18 | "kind": "xcode"
19 | }
--------------------------------------------------------------------------------