Merge branch 'buzzert/hubitat' into 'master'
Hubitat Support See merge request zanneth/XIONControlPanel!1
This commit is contained in:
@@ -36,6 +36,11 @@
|
|||||||
0CAA327D1C361ECA00B353CF /* StandardError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAA327C1C361ECA00B353CF /* StandardError.swift */; };
|
0CAA327D1C361ECA00B353CF /* StandardError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAA327C1C361ECA00B353CF /* StandardError.swift */; };
|
||||||
0CF0F8961C362D8F00FE7752 /* NSURLAdditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF0F8951C362D8F00FE7752 /* NSURLAdditions.swift */; };
|
0CF0F8961C362D8F00FE7752 /* NSURLAdditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF0F8951C362D8F00FE7752 /* NSURLAdditions.swift */; };
|
||||||
0CF9A48E1C35E9C800276F13 /* SwitchIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF9A48D1C35E9C800276F13 /* SwitchIndicatorView.swift */; };
|
0CF9A48E1C35E9C800276F13 /* SwitchIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF9A48D1C35E9C800276F13 /* SwitchIndicatorView.swift */; };
|
||||||
|
CD815B8E241C6F4000E2FA97 /* ServerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B8D241C6F4000E2FA97 /* ServerProtocol.swift */; };
|
||||||
|
CD815B90241C7CAC00E2FA97 /* DeviceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B8F241C7CAC00E2FA97 /* DeviceProtocol.swift */; };
|
||||||
|
CD815B92241DA28B00E2FA97 /* ServerMultiplex.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B91241DA28B00E2FA97 /* ServerMultiplex.swift */; };
|
||||||
|
CD815B94241DC16700E2FA97 /* HubitatServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B93241DC16700E2FA97 /* HubitatServer.swift */; };
|
||||||
|
CD815B96241DC44D00E2FA97 /* HubitatDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD815B95241DC44D00E2FA97 /* HubitatDevice.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
@@ -73,6 +78,11 @@
|
|||||||
0CAA327C1C361ECA00B353CF /* StandardError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StandardError.swift; sourceTree = "<group>"; };
|
0CAA327C1C361ECA00B353CF /* StandardError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StandardError.swift; sourceTree = "<group>"; };
|
||||||
0CF0F8951C362D8F00FE7752 /* NSURLAdditions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSURLAdditions.swift; sourceTree = "<group>"; };
|
0CF0F8951C362D8F00FE7752 /* NSURLAdditions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSURLAdditions.swift; sourceTree = "<group>"; };
|
||||||
0CF9A48D1C35E9C800276F13 /* SwitchIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchIndicatorView.swift; sourceTree = "<group>"; };
|
0CF9A48D1C35E9C800276F13 /* SwitchIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchIndicatorView.swift; sourceTree = "<group>"; };
|
||||||
|
CD815B8D241C6F4000E2FA97 /* ServerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerProtocol.swift; sourceTree = "<group>"; };
|
||||||
|
CD815B8F241C7CAC00E2FA97 /* DeviceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProtocol.swift; sourceTree = "<group>"; };
|
||||||
|
CD815B91241DA28B00E2FA97 /* ServerMultiplex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerMultiplex.swift; sourceTree = "<group>"; };
|
||||||
|
CD815B93241DC16700E2FA97 /* HubitatServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubitatServer.swift; sourceTree = "<group>"; };
|
||||||
|
CD815B95241DC44D00E2FA97 /* HubitatDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubitatDevice.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -112,7 +122,6 @@
|
|||||||
0C20E2BA1C33529700BA789B /* MainViewController.swift */,
|
0C20E2BA1C33529700BA789B /* MainViewController.swift */,
|
||||||
0C41EF491C34B51500AF847F /* SwitchesViewController.swift */,
|
0C41EF491C34B51500AF847F /* SwitchesViewController.swift */,
|
||||||
0C20E2BC1C33534F00BA789B /* VisualizationViewController.swift */,
|
0C20E2BC1C33534F00BA789B /* VisualizationViewController.swift */,
|
||||||
0CAA32781C361A2400B353CF /* WemoServer.swift */,
|
|
||||||
);
|
);
|
||||||
path = Controllers;
|
path = Controllers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -120,6 +129,8 @@
|
|||||||
0C20E2B71C33514100BA789B /* Models */ = {
|
0C20E2B71C33514100BA789B /* Models */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
CD815B8F241C7CAC00E2FA97 /* DeviceProtocol.swift */,
|
||||||
|
CD815B95241DC44D00E2FA97 /* HubitatDevice.swift */,
|
||||||
0C4314FF1C35B1E700164559 /* WemoDevice.swift */,
|
0C4314FF1C35B1E700164559 /* WemoDevice.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
@@ -169,6 +180,7 @@
|
|||||||
0C20E2B61C33514100BA789B /* Controllers */,
|
0C20E2B61C33514100BA789B /* Controllers */,
|
||||||
0C41EF4B1C34B81F00AF847F /* External */,
|
0C41EF4B1C34B81F00AF847F /* External */,
|
||||||
0C20E2B71C33514100BA789B /* Models */,
|
0C20E2B71C33514100BA789B /* Models */,
|
||||||
|
CD815B8C241C6F2C00E2FA97 /* Servers */,
|
||||||
0C63A8741C3373BC007E4B52 /* Shaders */,
|
0C63A8741C3373BC007E4B52 /* Shaders */,
|
||||||
0C20E2C81C336F2E00BA789B /* Resources */,
|
0C20E2C81C336F2E00BA789B /* Resources */,
|
||||||
0C63A8791C33C622007E4B52 /* Utilities */,
|
0C63A8791C33C622007E4B52 /* Utilities */,
|
||||||
@@ -215,6 +227,17 @@
|
|||||||
path = Utilities;
|
path = Utilities;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
CD815B8C241C6F2C00E2FA97 /* Servers */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
CD815B91241DA28B00E2FA97 /* ServerMultiplex.swift */,
|
||||||
|
CD815B8D241C6F4000E2FA97 /* ServerProtocol.swift */,
|
||||||
|
CD815B93241DC16700E2FA97 /* HubitatServer.swift */,
|
||||||
|
0CAA32781C361A2400B353CF /* WemoServer.swift */,
|
||||||
|
);
|
||||||
|
path = Servers;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@@ -242,13 +265,14 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastSwiftUpdateCheck = 0720;
|
LastSwiftUpdateCheck = 0720;
|
||||||
LastUpgradeCheck = 0720;
|
LastUpgradeCheck = 1200;
|
||||||
ORGANIZATIONNAME = XION;
|
ORGANIZATIONNAME = XION;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
0C3CAEA31C3350C800B856AD = {
|
0C3CAEA31C3350C800B856AD = {
|
||||||
CreatedOnToolsVersion = 7.2;
|
CreatedOnToolsVersion = 7.2;
|
||||||
DevelopmentTeam = F7W5R35V7L;
|
DevelopmentTeam = F7W5R35V7L;
|
||||||
LastSwiftMigration = 1010;
|
LastSwiftMigration = 1010;
|
||||||
|
ProvisioningStyle = Manual;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -301,10 +325,13 @@
|
|||||||
0C41EF4E1C34B81F00AF847F /* KSIRingView.m in Sources */,
|
0C41EF4E1C34B81F00AF847F /* KSIRingView.m in Sources */,
|
||||||
0CAA327B1C361CBE00B353CF /* NSErrorAdditions.swift in Sources */,
|
0CAA327B1C361CBE00B353CF /* NSErrorAdditions.swift in Sources */,
|
||||||
0CF0F8961C362D8F00FE7752 /* NSURLAdditions.swift in Sources */,
|
0CF0F8961C362D8F00FE7752 /* NSURLAdditions.swift in Sources */,
|
||||||
|
CD815B94241DC16700E2FA97 /* HubitatServer.swift in Sources */,
|
||||||
0CAA32791C361A2400B353CF /* WemoServer.swift in Sources */,
|
0CAA32791C361A2400B353CF /* WemoServer.swift in Sources */,
|
||||||
0C41EF561C35ABCD00AF847F /* ConnectionStatusView.swift in Sources */,
|
0C41EF561C35ABCD00AF847F /* ConnectionStatusView.swift in Sources */,
|
||||||
0C63A87D1C33C655007E4B52 /* NSValue+XIONAdditions.m in Sources */,
|
0C63A87D1C33C655007E4B52 /* NSValue+XIONAdditions.m in Sources */,
|
||||||
|
CD815B96241DC44D00E2FA97 /* HubitatDevice.swift in Sources */,
|
||||||
0C20E2BD1C33534F00BA789B /* VisualizationViewController.swift in Sources */,
|
0C20E2BD1C33534F00BA789B /* VisualizationViewController.swift in Sources */,
|
||||||
|
CD815B90241C7CAC00E2FA97 /* DeviceProtocol.swift in Sources */,
|
||||||
0C41EF4A1C34B51500AF847F /* SwitchesViewController.swift in Sources */,
|
0C41EF4A1C34B51500AF847F /* SwitchesViewController.swift in Sources */,
|
||||||
0C20E2B91C33514100BA789B /* AppDelegate.swift in Sources */,
|
0C20E2B91C33514100BA789B /* AppDelegate.swift in Sources */,
|
||||||
0C20E2BB1C33529700BA789B /* MainViewController.swift in Sources */,
|
0C20E2BB1C33529700BA789B /* MainViewController.swift in Sources */,
|
||||||
@@ -312,6 +339,8 @@
|
|||||||
0C21329B1C4DDC3E001E8F08 /* Semaphore.swift in Sources */,
|
0C21329B1C4DDC3E001E8F08 /* Semaphore.swift in Sources */,
|
||||||
0C41EF541C34BC9800AF847F /* XIONLogoView.swift in Sources */,
|
0C41EF541C34BC9800AF847F /* XIONLogoView.swift in Sources */,
|
||||||
0C4315061C35E67600164559 /* WemoCellView.swift in Sources */,
|
0C4315061C35E67600164559 /* WemoCellView.swift in Sources */,
|
||||||
|
CD815B92241DA28B00E2FA97 /* ServerMultiplex.swift in Sources */,
|
||||||
|
CD815B8E241C6F4000E2FA97 /* ServerProtocol.swift in Sources */,
|
||||||
0CAA327D1C361ECA00B353CF /* StandardError.swift in Sources */,
|
0CAA327D1C361ECA00B353CF /* StandardError.swift in Sources */,
|
||||||
0CF9A48E1C35E9C800276F13 /* SwitchIndicatorView.swift in Sources */,
|
0CF9A48E1C35E9C800276F13 /* SwitchIndicatorView.swift in Sources */,
|
||||||
0C4315001C35B1E700164559 /* WemoDevice.swift in Sources */,
|
0C4315001C35B1E700164559 /* WemoDevice.swift in Sources */,
|
||||||
@@ -336,17 +365,29 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
@@ -380,17 +421,29 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
@@ -408,6 +461,7 @@
|
|||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
};
|
};
|
||||||
@@ -418,14 +472,18 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "Apple Distribution: Charles Magahern (F7W5R35V7L)";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution: Charles Magahern (F7W5R35V7L)";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
CURRENT_PROJECT_VERSION = 3;
|
||||||
DEVELOPMENT_TEAM = F7W5R35V7L;
|
DEVELOPMENT_TEAM = F7W5R35V7L;
|
||||||
INFOPLIST_FILE = "$(SRCROOT)/XIONControlPanel/SupportingFiles/Info.plist";
|
INFOPLIST_FILE = "$(SRCROOT)/XIONControlPanel/SupportingFiles/Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
|
MARKETING_VERSION = 2.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.zanneth.XIONControlPanel;
|
PRODUCT_BUNDLE_IDENTIFIER = com.zanneth.XIONControlPanel;
|
||||||
PRODUCT_NAME = XION;
|
PRODUCT_NAME = XION;
|
||||||
PROVISIONING_PROFILE = "";
|
PROVISIONING_PROFILE = "";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "Octahedron II";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "XIONControlPanel/SupportingFiles/XIONControlPanel-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "XIONControlPanel/SupportingFiles/XIONControlPanel-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
@@ -437,14 +495,18 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "Apple Distribution: Charles Magahern (F7W5R35V7L)";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution: Charles Magahern (F7W5R35V7L)";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
CURRENT_PROJECT_VERSION = 3;
|
||||||
DEVELOPMENT_TEAM = F7W5R35V7L;
|
DEVELOPMENT_TEAM = F7W5R35V7L;
|
||||||
INFOPLIST_FILE = "$(SRCROOT)/XIONControlPanel/SupportingFiles/Info.plist";
|
INFOPLIST_FILE = "$(SRCROOT)/XIONControlPanel/SupportingFiles/Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
|
MARKETING_VERSION = 2.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.zanneth.XIONControlPanel;
|
PRODUCT_BUNDLE_IDENTIFIER = com.zanneth.XIONControlPanel;
|
||||||
PRODUCT_NAME = XION;
|
PRODUCT_NAME = XION;
|
||||||
PROVISIONING_PROFILE = "";
|
PROVISIONING_PROFILE = "";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "Octahedron II";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "XIONControlPanel/SupportingFiles/XIONControlPanel-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "XIONControlPanel/SupportingFiles/XIONControlPanel-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class MainViewController: UIViewController, SwitchesViewControllerDelegate
|
class MainViewController: UIViewController, SwitchesViewControllerDelegate, ServerMultiplexDelegate
|
||||||
{
|
{
|
||||||
fileprivate var _server: WemoServer
|
fileprivate var _serverMultiplex: ServerMultiplex = ServerMultiplex()
|
||||||
fileprivate var _visualizationController: VisualizationViewController = VisualizationViewController()
|
fileprivate var _visualizationController: VisualizationViewController = VisualizationViewController()
|
||||||
fileprivate var _switchesController: SwitchesViewController = SwitchesViewController()
|
fileprivate var _switchesController: SwitchesViewController = SwitchesViewController()
|
||||||
fileprivate var _headerView: HeaderView = HeaderView()
|
fileprivate var _headerView: HeaderView = HeaderView()
|
||||||
@@ -18,10 +18,11 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate
|
|||||||
|
|
||||||
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
|
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
|
||||||
{
|
{
|
||||||
let url = URL(string: "http://midna.xionsf.com:5000")!
|
|
||||||
_server = WemoServer(url)
|
|
||||||
|
|
||||||
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
|
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
|
||||||
|
|
||||||
|
_serverMultiplex.delegate = self
|
||||||
|
_serverMultiplex.addServer(HubitatServer(URL(string: "https://midna.xionsf.com:6000/apps/api/1/")!))
|
||||||
|
_serverMultiplex.addServer(WemoServer(URL(string: "http://midna.xionsf.com:5000")!))
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder)
|
required init?(coder aDecoder: NSCoder)
|
||||||
@@ -104,16 +105,11 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate
|
|||||||
|
|
||||||
_headerView.xionLogoView.beginAnimating()
|
_headerView.xionLogoView.beginAnimating()
|
||||||
|
|
||||||
if (!_server.connected) {
|
if _serverMultiplex.devices.count == 0 {
|
||||||
|
// Do initial refresh
|
||||||
_updateConnectivityStatus(.connecting)
|
_updateConnectivityStatus(.connecting)
|
||||||
_server.connect { (error: Error?) -> Void in
|
_serverMultiplex.refreshDevices()
|
||||||
if (error == nil) {
|
_startUpdatingDevices()
|
||||||
self._reloadDevices()
|
|
||||||
self._startUpdatingDevices()
|
|
||||||
} else {
|
|
||||||
self._updateConnectivityStatus(.error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,25 +130,14 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: API
|
|
||||||
|
|
||||||
var devices: [WemoDevice] = []
|
|
||||||
{
|
|
||||||
didSet
|
|
||||||
{
|
|
||||||
_switchesController.devices = devices
|
|
||||||
_updateVisualization(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: SwitchesViewControllerDelegate
|
// MARK: SwitchesViewControllerDelegate
|
||||||
|
|
||||||
func switchesViewControllerDidToggleDevices(_ controller: SwitchesViewController, devices: [WemoDevice])
|
func switchesViewControllerDidToggleDevices(_ controller: SwitchesViewController, devices: [AnyDevice])
|
||||||
{
|
{
|
||||||
_updateVisualization(true)
|
_updateVisualization(true)
|
||||||
|
|
||||||
for device in devices {
|
for device in devices {
|
||||||
_server.toggleDevice(device, state: device.state, completion: { (error: Error?) -> Void in })
|
_serverMultiplex.toggleDeviceState(device, state: device.state, completion: { (error: Error?) -> Void in })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,14 +164,10 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate
|
|||||||
|
|
||||||
internal func _updateVisualization(_ animated: Bool)
|
internal func _updateVisualization(_ animated: Bool)
|
||||||
{
|
{
|
||||||
var activatedDevicesCount = 0
|
let activatedDevicesCount = _serverMultiplex.devices.filter { $0.state == .on }.count
|
||||||
for device in self.devices {
|
let totalDeviceCount = _serverMultiplex.devices.count
|
||||||
if (device.state == .on) {
|
|
||||||
activatedDevicesCount += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let percentageActivated = (self.devices.count > 0 ? Float(activatedDevicesCount) / Float(self.devices.count) : 0.0)
|
let percentageActivated = (totalDeviceCount > 0 ? Float(activatedDevicesCount) / Float(totalDeviceCount) : 0.0)
|
||||||
if (percentageActivated != _visualizationController.percentActivated) {
|
if (percentageActivated != _visualizationController.percentActivated) {
|
||||||
_visualizationController.setPercentActivated(percentageActivated, animated: animated)
|
_visualizationController.setPercentActivated(percentageActivated, animated: animated)
|
||||||
}
|
}
|
||||||
@@ -201,21 +182,6 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal func _reloadDevices()
|
|
||||||
{
|
|
||||||
_server.fetchDevices({ (devices: [WemoDevice], error: Error?) -> Void in
|
|
||||||
DispatchQueue.main.async { () -> Void in
|
|
||||||
if (error == nil) {
|
|
||||||
self.devices = devices
|
|
||||||
self._updateConnectivityStatus(.connected)
|
|
||||||
} else {
|
|
||||||
self.devices = []
|
|
||||||
self._updateConnectivityStatus(.error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
internal func _startUpdatingDevices()
|
internal func _startUpdatingDevices()
|
||||||
{
|
{
|
||||||
_updateDevices = true
|
_updateDevices = true
|
||||||
@@ -223,7 +189,7 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate
|
|||||||
let interval = DispatchTime.now() + Double(Int64(10 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
|
let interval = DispatchTime.now() + Double(Int64(10 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
|
||||||
DispatchQueue.main.asyncAfter(deadline: interval) { () -> Void in
|
DispatchQueue.main.asyncAfter(deadline: interval) { () -> Void in
|
||||||
if (self._updateDevices) {
|
if (self._updateDevices) {
|
||||||
self._reloadDevices()
|
self._serverMultiplex.refreshDevices()
|
||||||
self._startUpdatingDevices()
|
self._startUpdatingDevices()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,4 +199,30 @@ class MainViewController: UIViewController, SwitchesViewControllerDelegate
|
|||||||
{
|
{
|
||||||
_updateDevices = false
|
_updateDevices = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Server Multiplex Delegate
|
||||||
|
|
||||||
|
func serverMultiplex(_ multiplex: ServerMultiplex, didAddDevices devices: [AnyDevice])
|
||||||
|
{
|
||||||
|
_switchesController.devices = Array(multiplex.devices)
|
||||||
|
_updateVisualization(false)
|
||||||
|
|
||||||
|
// Update connection status too, if applicable
|
||||||
|
if _serverMultiplex.numServers == _serverMultiplex.numberOfConnectedServers() {
|
||||||
|
_updateConnectivityStatus(.connected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverMultiplex(_ multiplex: ServerMultiplex, devicesStateChanged devices: [AnyDevice])
|
||||||
|
{
|
||||||
|
_switchesController.devicesStateChanged(devices)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverMultiplex(_ multiplex: ServerMultiplex, didEncounterError error: Error)
|
||||||
|
{
|
||||||
|
_updateConnectivityStatus(.error)
|
||||||
|
|
||||||
|
var stderr = StandardErrorOutputStream()
|
||||||
|
print(error.localizedDescription, to: &stderr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,29 +12,29 @@ import UIKit
|
|||||||
|
|
||||||
protocol SwitchesViewControllerDelegate: class
|
protocol SwitchesViewControllerDelegate: class
|
||||||
{
|
{
|
||||||
func switchesViewControllerDidToggleDevices(_ controller: SwitchesViewController, devices: [WemoDevice])
|
func switchesViewControllerDidToggleDevices(_ controller: SwitchesViewController, devices: [AnyDevice])
|
||||||
}
|
|
||||||
|
|
||||||
extension SwitchesViewControllerDelegate
|
|
||||||
{
|
|
||||||
func switchesViewControllerDidToggleDevices(_ controller: SwitchesViewController, devices: [WemoDevice]) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SwitchesViewController: UIViewController,
|
class SwitchesViewController: UIViewController,
|
||||||
UICollectionViewDataSource,
|
|
||||||
UICollectionViewDelegateFlowLayout
|
UICollectionViewDelegateFlowLayout
|
||||||
{
|
{
|
||||||
weak var delegate: SwitchesViewControllerDelegate?
|
weak var delegate: SwitchesViewControllerDelegate?
|
||||||
|
|
||||||
fileprivate var _collectionView: UICollectionView = UICollectionView(frame: CGRect.zero,
|
fileprivate var _collectionView: UICollectionView = UICollectionView(frame: CGRect.zero,
|
||||||
collectionViewLayout: UICollectionViewFlowLayout())
|
collectionViewLayout: UICollectionViewFlowLayout())
|
||||||
|
|
||||||
|
fileprivate var _collectionViewDataSource: UICollectionViewDiffableDataSource<Int, Int>!
|
||||||
|
|
||||||
fileprivate var _currentDevicesHash: Int = 0
|
fileprivate var _currentDevicesHash: Int = 0
|
||||||
|
|
||||||
static fileprivate let collectionViewDeviceSwitchCellReuseIdentifier = "DeviceSwitchReuseID"
|
static fileprivate let collectionViewDeviceSwitchCellReuseIdentifier = "DeviceSwitchReuseID"
|
||||||
static fileprivate let collectionViewActionCellReuseIdentifier = "ActionCellReuseID"
|
static fileprivate let collectionViewActionCellReuseIdentifier = "ActionCellReuseID"
|
||||||
static fileprivate let collectionViewCellsSpacing: CGFloat = 5.0
|
static fileprivate let collectionViewCellsSpacing: CGFloat = 5.0
|
||||||
|
|
||||||
fileprivate enum ActionCell: Int
|
static fileprivate let actionCellsSectionIdentifier = 0
|
||||||
|
static fileprivate let switchCellsSectionIdentifier = 1
|
||||||
|
|
||||||
|
fileprivate enum ActionCell: Int, CaseIterable
|
||||||
{
|
{
|
||||||
case allOn
|
case allOn
|
||||||
case allOff
|
case allOff
|
||||||
@@ -49,13 +49,37 @@ class SwitchesViewController: UIViewController,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static let count: Int = {
|
static let count: Int = { return ActionCell.allCases.count }()
|
||||||
var max = 0
|
|
||||||
while let _ = ActionCell(rawValue: max) { max += 1 }
|
|
||||||
return max
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
|
||||||
|
{
|
||||||
|
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
|
||||||
|
|
||||||
|
_collectionViewDataSource = UICollectionViewDiffableDataSource(collectionView: _collectionView, cellProvider: { (collectionView, indexPath, identifier) -> UICollectionViewCell? in
|
||||||
|
return self.collectionView(collectionView, cellForItemAt: indexPath, identifier: identifier)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initialize data source
|
||||||
|
var snapshot = _collectionViewDataSource.snapshot()
|
||||||
|
|
||||||
|
// Sections
|
||||||
|
snapshot.appendSections([
|
||||||
|
SwitchesViewController.actionCellsSectionIdentifier,
|
||||||
|
SwitchesViewController.switchCellsSectionIdentifier,
|
||||||
|
])
|
||||||
|
|
||||||
|
// Action cells
|
||||||
|
snapshot.appendItems([
|
||||||
|
SwitchesViewController.ActionCell.allOn.rawValue,
|
||||||
|
SwitchesViewController.ActionCell.allOff.rawValue
|
||||||
|
], toSection: SwitchesViewController.actionCellsSectionIdentifier)
|
||||||
|
|
||||||
|
_collectionViewDataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { fatalError("Unimplemented") }
|
||||||
|
|
||||||
override func viewDidLoad()
|
override func viewDidLoad()
|
||||||
{
|
{
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
@@ -69,7 +93,7 @@ class SwitchesViewController: UIViewController,
|
|||||||
|
|
||||||
_collectionView.backgroundColor = UIColor.black
|
_collectionView.backgroundColor = UIColor.black
|
||||||
_collectionView.delegate = self
|
_collectionView.delegate = self
|
||||||
_collectionView.dataSource = self
|
_collectionView.dataSource = _collectionViewDataSource
|
||||||
_collectionView.register(WemoDeviceCellView.self, forCellWithReuseIdentifier: deviceCellReuseID)
|
_collectionView.register(WemoDeviceCellView.self, forCellWithReuseIdentifier: deviceCellReuseID)
|
||||||
_collectionView.register(WemoActionCellView.self, forCellWithReuseIdentifier: actionCellReuseID)
|
_collectionView.register(WemoActionCellView.self, forCellWithReuseIdentifier: actionCellReuseID)
|
||||||
self.view.addSubview(_collectionView)
|
self.view.addSubview(_collectionView)
|
||||||
@@ -88,96 +112,60 @@ class SwitchesViewController: UIViewController,
|
|||||||
|
|
||||||
// MARK: API
|
// MARK: API
|
||||||
|
|
||||||
var devices: [WemoDevice] = []
|
var devices: [AnyDevice] = []
|
||||||
{
|
{
|
||||||
didSet
|
didSet
|
||||||
{
|
{
|
||||||
// sort devices by name
|
// sort devices by name
|
||||||
self.devices.sort(by: { (d1: WemoDevice, d2: WemoDevice) -> Bool in
|
self.devices.sort(by: { (d1: Device, d2: Device) -> Bool in
|
||||||
return (d1.name.compare(d2.name, options: .caseInsensitive, range: nil, locale: nil) == .orderedAscending)
|
return (d1.name.compare(d2.name, options: .caseInsensitive, range: nil, locale: nil) == .orderedAscending)
|
||||||
})
|
})
|
||||||
|
|
||||||
let hash = self.devices.reduce(0, {$0 ^ $1.hashValue})
|
var snapshot = _collectionViewDataSource.snapshot()
|
||||||
if (hash != _currentDevicesHash) {
|
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: SwitchesViewController.switchCellsSectionIdentifier))
|
||||||
let previousSet = NSOrderedSet(array: oldValue)
|
snapshot.appendItems(self.devices.map { $0.hashValue }, toSection: SwitchesViewController.switchCellsSectionIdentifier)
|
||||||
let newSet = NSOrderedSet(array: self.devices)
|
_collectionViewDataSource.apply(snapshot, animatingDifferences: false)
|
||||||
var insertedIndexPaths: [IndexPath] = []
|
|
||||||
var updatedIndexPaths: [IndexPath] = []
|
|
||||||
var deletedIndexPaths: [IndexPath] = []
|
|
||||||
|
|
||||||
// if we have devices now and we didn't before, or vice versa,
|
|
||||||
// we need to update the action cells
|
|
||||||
if ((oldValue.count == 0 && self.devices.count != 0) || (self.devices.count == 0 && oldValue.count != 0)) {
|
|
||||||
for actionCellIdx in 0 ..< ActionCell.count {
|
|
||||||
let actionCellIndexPath = IndexPath(item: actionCellIdx, section: 0)
|
|
||||||
updatedIndexPaths.append(actionCellIndexPath)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// find deletes and updates
|
public func devicesStateChanged(_ changedDevices: [AnyDevice])
|
||||||
for (idx, device) in previousSet.enumerated() {
|
{
|
||||||
let itemIndex = idx + ActionCell.count
|
var snapshot = _collectionViewDataSource.snapshot()
|
||||||
let curIndexPath = IndexPath(item: itemIndex, section: 0)
|
changedDevices.forEach { changedDevice in
|
||||||
|
if let existingDevice = (self.devices.first { $0.hashValue == changedDevice.hashValue }) {
|
||||||
if (!newSet.contains(device)) {
|
existingDevice.state = changedDevice.state
|
||||||
deletedIndexPaths.append(curIndexPath)
|
snapshot.reloadItems([ existingDevice.hashValue ])
|
||||||
} else if (idx < newSet.count) {
|
|
||||||
let deviceInNewSet = newSet.object(at: idx) as! WemoDevice
|
|
||||||
if (deviceInNewSet != (device as! WemoDevice)) {
|
|
||||||
updatedIndexPaths.append(curIndexPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// find insertions
|
|
||||||
for (idx, device) in newSet.enumerated() {
|
|
||||||
if (!previousSet.contains(device)) {
|
|
||||||
let itemIndex = idx + ActionCell.count
|
|
||||||
let insertedIndexPath = IndexPath(item: itemIndex, section: 0)
|
|
||||||
insertedIndexPaths.append(insertedIndexPath)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UIView.performWithoutAnimation { () -> Void in
|
_collectionViewDataSource.apply(snapshot, animatingDifferences: true)
|
||||||
self._collectionView.performBatchUpdates({ () -> Void in
|
|
||||||
self._collectionView.deleteItems(at: deletedIndexPaths)
|
|
||||||
self._collectionView.reloadItems(at: updatedIndexPaths)
|
|
||||||
self._collectionView.insertItems(at: insertedIndexPaths)
|
|
||||||
}, completion: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentDevicesHash = hash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: UICollectionView
|
// MARK: UICollectionView
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
|
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, identifier: Int) -> UICollectionViewCell?
|
||||||
{
|
{
|
||||||
return self.devices.count + ActionCell.count
|
if (indexPath.section == SwitchesViewController.actionCellsSectionIdentifier) {
|
||||||
}
|
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
|
|
||||||
{
|
|
||||||
if (indexPath.item < ActionCell.count) {
|
|
||||||
let reuseID = SwitchesViewController.collectionViewActionCellReuseIdentifier
|
let reuseID = SwitchesViewController.collectionViewActionCellReuseIdentifier
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseID, for: indexPath) as! WemoActionCellView
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseID, for: indexPath) as! WemoActionCellView
|
||||||
cell.textLabel.text = ActionCell(rawValue: indexPath.item)?.name().uppercased()
|
cell.textLabel.text = ActionCell(rawValue: identifier)?.name().uppercased()
|
||||||
cell.enabled = (self.devices.count > 0)
|
cell.enabled = (self.devices.count > 0)
|
||||||
|
|
||||||
return cell
|
return cell
|
||||||
} else {
|
} else if (indexPath.section == SwitchesViewController.switchCellsSectionIdentifier) {
|
||||||
let reuseID = SwitchesViewController.collectionViewDeviceSwitchCellReuseIdentifier
|
let reuseID = SwitchesViewController.collectionViewDeviceSwitchCellReuseIdentifier
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseID, for: indexPath) as! WemoDeviceCellView
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseID, for: indexPath) as! WemoDeviceCellView
|
||||||
|
|
||||||
let device = _deviceAtIndexPath(indexPath)
|
if let device = (self.devices.first { $0.hashValue == identifier }) {
|
||||||
cell.deviceName = device.name
|
cell.deviceName = device.name
|
||||||
cell.toggled = (device.state == .on)
|
cell.toggled = (device.state == .on)
|
||||||
cell.ordinal = indexPath.item - ActionCell.count + 1
|
cell.ordinal = indexPath.row
|
||||||
|
}
|
||||||
|
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView,
|
func collectionView(_ collectionView: UICollectionView,
|
||||||
@@ -206,28 +194,33 @@ class SwitchesViewController: UIViewController,
|
|||||||
}
|
}
|
||||||
|
|
||||||
let dimensions = floor((collectionView.bounds.size.width / cellsPerRow) - ((spacing * (cellsPerRow - 1.0)) / cellsPerRow))
|
let dimensions = floor((collectionView.bounds.size.width / cellsPerRow) - ((spacing * (cellsPerRow - 1.0)) / cellsPerRow))
|
||||||
if (indexPath.item < ActionCell.count) {
|
if (indexPath.section == SwitchesViewController.actionCellsSectionIdentifier) {
|
||||||
return CGSize(width: collectionView.bounds.size.width, height: rint(dimensions / 1.5))
|
return CGSize(width: collectionView.bounds.size.width, height: rint(dimensions / 1.5))
|
||||||
} else {
|
} else {
|
||||||
return CGSize(width: dimensions, height: dimensions)
|
return CGSize(width: dimensions, height: dimensions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
|
||||||
|
{
|
||||||
|
let spacing = SwitchesViewController.collectionViewCellsSpacing
|
||||||
|
return UIEdgeInsets(top: spacing, left: 0, bottom: 0, right: 0)
|
||||||
|
}
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
|
||||||
{
|
{
|
||||||
if (indexPath.item < ActionCell.count) {
|
if (indexPath.section == SwitchesViewController.actionCellsSectionIdentifier) {
|
||||||
let tappedActionCell = ActionCell(rawValue: indexPath.item)
|
let tappedActionCell = ActionCell(rawValue: indexPath.item)
|
||||||
var currentDelay: TimeInterval = 0.0
|
|
||||||
|
|
||||||
for i in ActionCell.count ..< collectionView.numberOfItems(inSection: indexPath.section) {
|
for cell in collectionView.visibleCells {
|
||||||
if let cell = collectionView.cellForItem(at: IndexPath(item: i, section: indexPath.section)) as? WemoDeviceCellView {
|
if collectionView.indexPath(for: cell)?.section == SwitchesViewController.switchCellsSectionIdentifier {
|
||||||
|
let switchCell = cell as! WemoDeviceCellView
|
||||||
let animOptions = UIView.AnimationOptions([.allowUserInteraction])
|
let animOptions = UIView.AnimationOptions([.allowUserInteraction])
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.3, delay: currentDelay, options: animOptions, animations: {
|
let delay: TimeInterval = Double(switchCell.ordinal) * 0.05
|
||||||
cell.toggled = (tappedActionCell == .allOn)
|
UIView.animate(withDuration: 0.3, delay: delay, options: animOptions, animations: {
|
||||||
|
switchCell.toggled = (tappedActionCell == .allOn)
|
||||||
}, completion: nil)
|
}, completion: nil)
|
||||||
|
|
||||||
currentDelay += 0.05
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,11 +229,11 @@ class SwitchesViewController: UIViewController,
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.delegate?.switchesViewControllerDidToggleDevices(self, devices: self.devices)
|
self.delegate?.switchesViewControllerDidToggleDevices(self, devices: self.devices)
|
||||||
} else {
|
} else if (indexPath.section == SwitchesViewController.switchCellsSectionIdentifier) {
|
||||||
let cell = collectionView.cellForItem(at: indexPath) as! WemoDeviceCellView
|
let cell = collectionView.cellForItem(at: indexPath) as! WemoDeviceCellView
|
||||||
cell.toggled = !cell.toggled
|
cell.toggled = !cell.toggled
|
||||||
|
|
||||||
let device = _deviceAtIndexPath(indexPath)
|
let device = self.devices[indexPath.row]
|
||||||
device.state = (cell.toggled ? .on : .off)
|
device.state = (cell.toggled ? .on : .off)
|
||||||
|
|
||||||
self.delegate?.switchesViewControllerDidToggleDevices(self, devices: [device])
|
self.delegate?.switchesViewControllerDidToggleDevices(self, devices: [device])
|
||||||
@@ -249,28 +242,15 @@ class SwitchesViewController: UIViewController,
|
|||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool
|
func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool
|
||||||
{
|
{
|
||||||
if (indexPath.item < ActionCell.count) {
|
return self.collectionView(collectionView, shouldSelectItemAt: indexPath)
|
||||||
return (self.devices.count > 0)
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool
|
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool
|
||||||
{
|
{
|
||||||
if (indexPath.item < ActionCell.count) {
|
if (indexPath.section == SwitchesViewController.actionCellsSectionIdentifier) {
|
||||||
return (self.devices.count > 0)
|
return (self.devices.count > 0)
|
||||||
} else {
|
} else {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Internal
|
|
||||||
|
|
||||||
internal func _deviceAtIndexPath(_ indexPath: IndexPath) -> WemoDevice
|
|
||||||
{
|
|
||||||
let deviceIdx = indexPath.item - ActionCell.count
|
|
||||||
let device = self.devices[deviceIdx]
|
|
||||||
return device
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
72
XIONControlPanel/Models/DeviceProtocol.swift
Normal file
72
XIONControlPanel/Models/DeviceProtocol.swift
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
//
|
||||||
|
// DeviceProtocol.swift
|
||||||
|
// XIONControlPanel
|
||||||
|
//
|
||||||
|
// Created by James Magahern on 3/13/20.
|
||||||
|
// Copyright © 2020 XION. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum DeviceState : String, Codable
|
||||||
|
{
|
||||||
|
case off
|
||||||
|
case on
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DeviceType : String, Codable
|
||||||
|
{
|
||||||
|
case `switch`
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol Device
|
||||||
|
{
|
||||||
|
// Read-only properties
|
||||||
|
var name: String { get }
|
||||||
|
var serial: String { get }
|
||||||
|
var type: DeviceType { get }
|
||||||
|
|
||||||
|
// Writable properties
|
||||||
|
var state: DeviceState { get set }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Device where Self: Hashable
|
||||||
|
{
|
||||||
|
func hash(into hasher: inout Hasher)
|
||||||
|
{
|
||||||
|
hasher.combine(self.name)
|
||||||
|
hasher.combine(self.type)
|
||||||
|
hasher.combine(self.serial)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: Self, rhs: Self) -> Bool
|
||||||
|
{
|
||||||
|
return lhs.name == rhs.name && lhs.serial == rhs.serial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnyDevice : Device
|
||||||
|
{
|
||||||
|
private var device: Device
|
||||||
|
|
||||||
|
public var name: String
|
||||||
|
{ return device.name }
|
||||||
|
|
||||||
|
public var serial: String
|
||||||
|
{ return device.serial }
|
||||||
|
|
||||||
|
public var type: DeviceType
|
||||||
|
{ return device.type }
|
||||||
|
|
||||||
|
public var state: DeviceState {
|
||||||
|
get { return device.state }
|
||||||
|
set { device.state = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ device: Device)
|
||||||
|
{
|
||||||
|
self.device = device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AnyDevice : Hashable {}
|
||||||
49
XIONControlPanel/Models/HubitatDevice.swift
Normal file
49
XIONControlPanel/Models/HubitatDevice.swift
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
//
|
||||||
|
// HubitatDevice.swift
|
||||||
|
// XIONControlPanel
|
||||||
|
//
|
||||||
|
// Created by James Magahern on 3/14/20.
|
||||||
|
// Copyright © 2020 XION. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct HubitatDevice : Device
|
||||||
|
{
|
||||||
|
var name: String = ""
|
||||||
|
var serial: String = ""
|
||||||
|
var type: DeviceType = .switch
|
||||||
|
var state: DeviceState = .off
|
||||||
|
|
||||||
|
var id: Int = -1
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey
|
||||||
|
{
|
||||||
|
case name = "label"
|
||||||
|
case id = "id"
|
||||||
|
case attributes = "attributes"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AttributesKeys: String, CodingKey
|
||||||
|
{
|
||||||
|
case power
|
||||||
|
case dataType
|
||||||
|
case `switch`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HubitatDevice : Decodable
|
||||||
|
{
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
|
let idStr = try values.decode(String.self, forKey: .id)
|
||||||
|
id = Int(idStr)!
|
||||||
|
serial = String(format: "hubitat:%d", id)
|
||||||
|
|
||||||
|
name = try values.decode(String.self, forKey: .name)
|
||||||
|
|
||||||
|
let attributes = try values.nestedContainer(keyedBy: Self.AttributesKeys, forKey: .attributes)
|
||||||
|
state = try attributes.decode(DeviceState.self, forKey: .switch)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,23 +8,12 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class WemoDevice: Hashable
|
class WemoDevice : Device
|
||||||
{
|
{
|
||||||
enum State
|
|
||||||
{
|
|
||||||
case off
|
|
||||||
case on
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DeviceType
|
|
||||||
{
|
|
||||||
case `switch`
|
|
||||||
}
|
|
||||||
|
|
||||||
var name: String = ""
|
var name: String = ""
|
||||||
var host: String = ""
|
var host: String = ""
|
||||||
var model: String = ""
|
var model: String = ""
|
||||||
var state: State = .off
|
var state: DeviceState = .off
|
||||||
var type: DeviceType = .switch
|
var type: DeviceType = .switch
|
||||||
var serial: String = ""
|
var serial: String = ""
|
||||||
|
|
||||||
|
|||||||
107
XIONControlPanel/Servers/HubitatServer.swift
Normal file
107
XIONControlPanel/Servers/HubitatServer.swift
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
//
|
||||||
|
// HubitatServer.swift
|
||||||
|
// XIONControlPanel
|
||||||
|
//
|
||||||
|
// Created by James Magahern on 3/14/20.
|
||||||
|
// Copyright © 2020 XION. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
public enum HubitatServerError : Error
|
||||||
|
{
|
||||||
|
case apiError
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HubitatServer : Server
|
||||||
|
{
|
||||||
|
var connected: Bool = false
|
||||||
|
|
||||||
|
public fileprivate(set) var devices: [HubitatDevice] = []
|
||||||
|
|
||||||
|
private let baseURL: URL
|
||||||
|
private let accessToken = "fa861583-71f0-466f-8add-f29b2278af6a"
|
||||||
|
private var fetchCancellable: AnyCancellable?
|
||||||
|
private var switchCancellable: AnyCancellable?
|
||||||
|
|
||||||
|
required init(_ url: URL)
|
||||||
|
{
|
||||||
|
self.baseURL = url
|
||||||
|
}
|
||||||
|
|
||||||
|
func connect(_ completion: @escaping (Error?) -> Void)
|
||||||
|
{
|
||||||
|
// RESTful, assume we're connected unless we get an error
|
||||||
|
self.connected = true
|
||||||
|
completion(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func disconnect(_ completion: (Error?) -> Void)
|
||||||
|
{
|
||||||
|
self.connected = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchDevices(_ fetchCompletion: @escaping (Result<[AnyDevice], Error>) -> Void)
|
||||||
|
{
|
||||||
|
let url = baseURL.appendingPathComponent("devices/all").authenticatedURLWithAccessToken(accessToken)
|
||||||
|
|
||||||
|
self.fetchCancellable = URLSession.shared.dataTaskPublisher(for: url)
|
||||||
|
.mapError { $0 as Error }
|
||||||
|
.map { $0.data }
|
||||||
|
.decode(type: [HubitatDevice].self, decoder: JSONDecoder())
|
||||||
|
.sink(receiveCompletion: { completion in
|
||||||
|
if case let Subscribers.Completion.failure(error) = completion {
|
||||||
|
self.connected = false
|
||||||
|
fetchCompletion(.failure(error))
|
||||||
|
} else {
|
||||||
|
self.connected = true
|
||||||
|
}
|
||||||
|
}) { (devices: [HubitatDevice]) in
|
||||||
|
self.devices = devices
|
||||||
|
fetchCompletion(.success(devices.map { AnyDevice($0) }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggleDevice(_ device: AnyDevice, state: DeviceState, completion toggleCompletion: @escaping (Error?) -> Void)
|
||||||
|
{
|
||||||
|
if let hubitatDevice = (self.devices.first { $0.serial == device.serial }) {
|
||||||
|
let command = "devices/\(hubitatDevice.id)/\(state)"
|
||||||
|
let url = baseURL.appendingPathComponent(command).authenticatedURLWithAccessToken(accessToken)
|
||||||
|
|
||||||
|
self.switchCancellable = URLSession.shared.dataTaskPublisher(for: url)
|
||||||
|
.tryMap { data, response -> Data in
|
||||||
|
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
|
||||||
|
throw HubitatServerError.apiError
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
.decode(type: HubitatDevice.self, decoder: JSONDecoder())
|
||||||
|
.sink(receiveCompletion: { (completion: Subscribers.Completion<Error>) in
|
||||||
|
if case let Subscribers.Completion.failure(error) = completion {
|
||||||
|
toggleCompletion(error)
|
||||||
|
}
|
||||||
|
}, receiveValue: { (device: HubitatDevice) in
|
||||||
|
// don't need to do anything with this, but the server returns it to us.
|
||||||
|
toggleCompletion(nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func responsibleForDevice(_ device: AnyDevice) -> Bool
|
||||||
|
{
|
||||||
|
return self.devices.contains { $0.serial == device.serial }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension URL
|
||||||
|
{
|
||||||
|
func authenticatedURLWithAccessToken(_ token: String) -> URL
|
||||||
|
{
|
||||||
|
guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { fatalError() }
|
||||||
|
components.queryItems = [ URLQueryItem(name: "access_token", value: token) ]
|
||||||
|
|
||||||
|
return components.url!
|
||||||
|
}
|
||||||
|
}
|
||||||
110
XIONControlPanel/Servers/ServerMultiplex.swift
Normal file
110
XIONControlPanel/Servers/ServerMultiplex.swift
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
//
|
||||||
|
// ServerMultiplex.swift
|
||||||
|
// XIONControlPanel
|
||||||
|
//
|
||||||
|
// Created by James Magahern on 3/14/20.
|
||||||
|
// Copyright © 2020 XION. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol ServerMultiplexDelegate
|
||||||
|
{
|
||||||
|
func serverMultiplex(_ multiplex: ServerMultiplex, didAddDevices devices: [AnyDevice])
|
||||||
|
func serverMultiplex(_ multiplex: ServerMultiplex, devicesStateChanged devices: [AnyDevice])
|
||||||
|
func serverMultiplex(_ multiplex: ServerMultiplex, didEncounterError error: Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ServerMultiplexError : Error
|
||||||
|
{
|
||||||
|
case unknownDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServerMultiplex
|
||||||
|
{
|
||||||
|
public var delegate: ServerMultiplexDelegate?
|
||||||
|
public private(set) var devices = Set<AnyDevice>()
|
||||||
|
|
||||||
|
private var servers: [Server] = []
|
||||||
|
public var numServers: Int { return servers.count }
|
||||||
|
|
||||||
|
public func addServer(_ server: Server)
|
||||||
|
{
|
||||||
|
servers.append(server)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func numberOfConnectedServers() -> Int
|
||||||
|
{
|
||||||
|
return servers.filter { $0.connected == true }.count
|
||||||
|
}
|
||||||
|
|
||||||
|
public func toggleDeviceState(_ device: AnyDevice, state: DeviceState, completion: @escaping (Error?) -> Void)
|
||||||
|
{
|
||||||
|
if let server = (servers.first { $0.responsibleForDevice(device) }) {
|
||||||
|
server.toggleDevice(device, state: state, completion: completion)
|
||||||
|
|
||||||
|
// Update internal state
|
||||||
|
if let index = devices.firstIndex(of: device) {
|
||||||
|
devices[index].state = state
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
completion(ServerMultiplexError.unknownDevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func refreshDevices()
|
||||||
|
{
|
||||||
|
self.servers.forEach { (server: Server) in
|
||||||
|
server.fetchDevices { (result: Result<[AnyDevice], Error>) in
|
||||||
|
self.handleServerFetchResult(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ServerMultiplex
|
||||||
|
{
|
||||||
|
private func handleServerFetchResult(_ result: Result<[AnyDevice], Error>)
|
||||||
|
{
|
||||||
|
switch result {
|
||||||
|
case .success(let devices):
|
||||||
|
handleDevicesChanged(devices)
|
||||||
|
case .failure(let error):
|
||||||
|
handleError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleDevicesChanged(_ devicesChanged: [AnyDevice])
|
||||||
|
{
|
||||||
|
let newDevicesSet = Set<AnyDevice>(devicesChanged)
|
||||||
|
let additions = newDevicesSet.subtracting(self.devices)
|
||||||
|
let changed = newDevicesSet.filter { (device: AnyDevice) in
|
||||||
|
if let existing = (devices.first { $0.hashValue == device.hashValue }) {
|
||||||
|
return existing.state != device.state
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
self.devices = self.devices.union(newDevicesSet)
|
||||||
|
|
||||||
|
if additions.count > 0 {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.delegate?.serverMultiplex(self, didAddDevices: Array(additions))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if changed.count > 0 {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.delegate?.serverMultiplex(self, devicesStateChanged: Array(changed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleError(_ error: Error)
|
||||||
|
{
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.delegate?.serverMultiplex(self, didEncounterError: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
XIONControlPanel/Servers/ServerProtocol.swift
Normal file
33
XIONControlPanel/Servers/ServerProtocol.swift
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
//
|
||||||
|
// ServerProtocol.swift
|
||||||
|
// XIONControlPanel
|
||||||
|
//
|
||||||
|
// Created by James Magahern on 3/13/20.
|
||||||
|
// Copyright © 2020 XION. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol Server
|
||||||
|
{
|
||||||
|
var connected: Bool { get }
|
||||||
|
|
||||||
|
/// Designated initializer. Takes an API endpoint URL
|
||||||
|
init(_ url: URL)
|
||||||
|
|
||||||
|
/// Attempts to connect to the server at the specified endpoint.
|
||||||
|
func connect(_ completion: @escaping (Error?) -> Void)
|
||||||
|
|
||||||
|
/// Sets server's state as "disconnected"
|
||||||
|
func disconnect(_ completion: (Error?) -> Void)
|
||||||
|
|
||||||
|
/// Fetches a list of all currently configured devices for this server, or returns an error.
|
||||||
|
func fetchDevices(_ completion: @escaping (Result<[AnyDevice], Error>) -> Void)
|
||||||
|
|
||||||
|
/// Tells the server to set the state of a given device to the given state
|
||||||
|
func toggleDevice(_ device: AnyDevice, state: DeviceState, completion: @escaping (Error?) -> Void)
|
||||||
|
|
||||||
|
/// Returns true if this is a device this server is responsible for
|
||||||
|
func responsibleForDevice(_ device: AnyDevice) -> Bool
|
||||||
|
}
|
||||||
|
|
||||||
@@ -22,16 +22,20 @@ enum ConnectionError : Error
|
|||||||
case serverUnavailable
|
case serverUnavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
class WemoServer
|
class WemoServer : Server
|
||||||
{
|
{
|
||||||
|
private var devices: [WemoDevice] = []
|
||||||
|
|
||||||
|
public var connected: Bool { get { return self.connectionStatus == .connected } }
|
||||||
|
|
||||||
fileprivate(set) var baseURL: URL
|
fileprivate(set) var baseURL: URL
|
||||||
fileprivate(set) var connected: Bool = false
|
fileprivate(set) var connectionStatus: ConnectionStatus = .disconnected
|
||||||
|
|
||||||
fileprivate var _urlSession: URLSession
|
fileprivate var _urlSession: URLSession
|
||||||
fileprivate var _errorStream: StandardErrorOutputStream = StandardErrorOutputStream()
|
fileprivate var _errorStream: StandardErrorOutputStream = StandardErrorOutputStream()
|
||||||
fileprivate var _operationQueue: OperationQueue = OperationQueue()
|
fileprivate var _operationQueue: OperationQueue = OperationQueue()
|
||||||
|
|
||||||
init(_ url: URL)
|
required init(_ url: URL)
|
||||||
{
|
{
|
||||||
self.baseURL = url
|
self.baseURL = url
|
||||||
|
|
||||||
@@ -43,15 +47,16 @@ class WemoServer
|
|||||||
|
|
||||||
func connect(_ completion: @escaping (Error?) -> Void)
|
func connect(_ completion: @escaping (Error?) -> Void)
|
||||||
{
|
{
|
||||||
if (!self.connected) {
|
if (self.connectionStatus == .disconnected) {
|
||||||
let op = ConnectOperation(baseURL: self.baseURL, session: _urlSession)
|
let op = ConnectOperation(baseURL: self.baseURL, session: _urlSession)
|
||||||
weak var weakOp = op
|
weak var weakOp = op
|
||||||
op.completionBlock = {
|
op.completionBlock = {
|
||||||
guard let strongOp = weakOp else { completion(nil) ; return }
|
guard let strongOp = weakOp else { completion(nil) ; return }
|
||||||
if let error = strongOp.error {
|
if let error = strongOp.error {
|
||||||
self._logError("Error connecting to server", error: error)
|
self._logError("Error connecting to server", error: error)
|
||||||
|
self.connectionStatus = .disconnected
|
||||||
} else {
|
} else {
|
||||||
self.connected = true
|
self.connectionStatus = .connected
|
||||||
}
|
}
|
||||||
|
|
||||||
completion(strongOp.error)
|
completion(strongOp.error)
|
||||||
@@ -64,33 +69,41 @@ class WemoServer
|
|||||||
|
|
||||||
func disconnect(_ completion: (Error?) -> Void)
|
func disconnect(_ completion: (Error?) -> Void)
|
||||||
{
|
{
|
||||||
self.connected = false
|
self.connectionStatus = .disconnected
|
||||||
completion(nil)
|
completion(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchDevices(_ completion: @escaping ([WemoDevice], Error?) -> Void)
|
func fetchDevices(_ completion: @escaping (Result<[AnyDevice], Error>) -> Void)
|
||||||
{
|
{
|
||||||
if (self.connected) {
|
|
||||||
let op = FetchDevicesOperation(baseURL: self.baseURL, session: _urlSession)
|
let op = FetchDevicesOperation(baseURL: self.baseURL, session: _urlSession)
|
||||||
weak var weakOp = op
|
op.completionBlock = { [unowned op] in
|
||||||
op.completionBlock = {
|
if let error = op.error {
|
||||||
guard let strongOp = weakOp else { completion([], nil) ; return }
|
|
||||||
if let error = strongOp.error {
|
|
||||||
self._logError("Error fetching devices", error: error)
|
self._logError("Error fetching devices", error: error)
|
||||||
|
completion(.failure(error))
|
||||||
|
} else {
|
||||||
|
self.devices = op.devices
|
||||||
}
|
}
|
||||||
|
|
||||||
completion(strongOp.devices, strongOp.error)
|
self.devices = op.devices
|
||||||
|
completion(.success(self.devices.map { AnyDevice($0) }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.connectionStatus == .disconnected {
|
||||||
|
connect { error in
|
||||||
|
if error == nil {
|
||||||
|
self._operationQueue.addOperation(op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if self.connectionStatus == .connected {
|
||||||
_operationQueue.addOperation(op)
|
_operationQueue.addOperation(op)
|
||||||
} else {
|
} else {
|
||||||
let err = ConnectionError.serverUnavailable
|
completion(.failure(ConnectionError.serverUnavailable))
|
||||||
completion([], err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggleDevice(_ device: WemoDevice, state: WemoDevice.State, completion: @escaping (Error?) -> Void)
|
func toggleDevice(_ device: AnyDevice, state: DeviceState, completion: @escaping (Error?) -> Void)
|
||||||
{
|
{
|
||||||
if (self.connected) {
|
if self.connected, let device = findDevice(device) {
|
||||||
let op = ToggleDeviceOperation(baseURL: self.baseURL, session: _urlSession, device: device, state: state)
|
let op = ToggleDeviceOperation(baseURL: self.baseURL, session: _urlSession, device: device, state: state)
|
||||||
weak var weakOp = op
|
weak var weakOp = op
|
||||||
op.completionBlock = {
|
op.completionBlock = {
|
||||||
@@ -108,12 +121,22 @@ class WemoServer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func responsibleForDevice(_ device: AnyDevice) -> Bool
|
||||||
|
{
|
||||||
|
return self.devices.contains { $0.serial == device.serial }
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Internal
|
// MARK: Internal
|
||||||
|
|
||||||
internal func _logError(_ description: String, error: Error)
|
internal func _logError(_ description: String, error: Error)
|
||||||
{
|
{
|
||||||
print("ERROR: \(description) \(error)", to: &_errorStream)
|
print("ERROR: \(description) \(error)", to: &_errorStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal func findDevice(_ device: AnyDevice) -> WemoDevice?
|
||||||
|
{
|
||||||
|
return self.devices.first { $0.serial == device.serial }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class WemoOperation : Operation
|
internal class WemoOperation : Operation
|
||||||
@@ -188,9 +211,9 @@ internal class FetchDevicesOperation : WemoOperation
|
|||||||
internal class ToggleDeviceOperation : WemoOperation
|
internal class ToggleDeviceOperation : WemoOperation
|
||||||
{
|
{
|
||||||
var device: WemoDevice
|
var device: WemoDevice
|
||||||
var state: WemoDevice.State
|
var state: DeviceState
|
||||||
|
|
||||||
init(baseURL: URL, session: URLSession, device: WemoDevice, state: WemoDevice.State)
|
init(baseURL: URL, session: URLSession, device: WemoDevice, state: DeviceState)
|
||||||
{
|
{
|
||||||
self.device = device
|
self.device = device
|
||||||
self.state = state
|
self.state = state
|
||||||
@@ -15,11 +15,11 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
|
|||||||
Reference in New Issue
Block a user