//
// © 2018-2020 Qualcomm Technologies, Inc. and/or its subsidiaries. All rights reserved.
//

#import "IoTDevice.h"
#import "IoTDevice+Private.h"
#import "IoTPlayer+Private.h"
#import "NSError+IoTErrors.h"
#import "IoTSysController.h"
#import "IoTProperties.h"
#import "IoTGlobals.h"
#import "IoTWrappedRepresentation.h"
#import "IoTDeviceState.h"
#import "IoTServiceDiscoveryResult.h"
#import "IoTTypes.h"
#import "IoTFriendlyNameMapper.h"

#define RETURN_GETTER(type,attr,property,default) \
type ret = default; \
if (self.state.attr) { \
    ret = self.state.attr.property; \
} \
return ret;

#define ACTION(method) \
    BOOL success = [self.iotSysController method:^(BOOL success) { \
		if (completion && queue) { \
			dispatch_async(queue, ^{ \
				completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]); \
			}); \
		} \
	} completionQueue:self.backgroundQueue]; \
	if (!success && completion && queue) { \
		dispatch_async(queue, ^{ \
			completion(NO, [NSError errorWithIoTError:IoTErrorNotSupported description:nil]); \
		}); \
	} \

#define SETTER(attr,property,variable,method) \
if (!self.state.attr) { \
    if (completion && queue) { \
        dispatch_async(queue, ^{ \
            completion(NO, [NSError errorWithIoTError:IoTErrorNotSupported description:nil]); \
        }); \
    } \
} else { \
    typeof(self.state.attr) attrCopy = [self.state.attr copy]; \
	attrCopy.property = variable; \
	BOOL success = [self.iotSysController method:attrCopy \
										completion:^(BOOL success) { \
		if (completion && queue) { \
			dispatch_async(queue, ^{ \
				completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]); \
			}); \
		} \
	} completionQueue:self.backgroundQueue];  \
	if (!success && completion && queue) { \
		dispatch_async(queue, ^{ \
			completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]); \
		}); \
	} \
}

@implementation IoTDevicePassword
- (instancetype)init
{
    self = [super init];
    if (self) {
        self->_cancel = YES;
    }
    return self;
}

- (instancetype)initWithCancel:(BOOL)cancel password:(NSString *)password
{
    self = [self init];
    if (self) {
        self->_cancel = cancel;
        self->_password = [password copy];
    }
    return self;
}

@end

@implementation IoTBluetoothDevice

- (instancetype)init
{
    self = [super init];
    if (self) {
        self->_name = @"";
        self->_address = @"";
    }
    return self;
}

- (instancetype)initWithName:(NSString *)name address:(NSString *)address
{
    self = [self init];
    if (self) {
        self->_name = [name copy];
        self->_address = [address copy];
    }
    return self;
}

- (BOOL)isEqual:(id)object
{
    if (self == object) {
        return YES;
    }

    if (![object isKindOfClass:[self class]]) {
        return NO;
    }
    return self.hash == ((IoTBluetoothDevice *)object).hash;
}

- (NSUInteger)hash {
    return self.address.hash;
}

@end


@implementation IoTZigbeeDevice

- (instancetype)init
{
    self = [super init];
    if (self) {
        self->_name = @"";
        self->_identifier = 0;
        self->_type = IoTZigbeeDeviceTypeUnknown;
    }
    return self;
}

- (instancetype)initWithName:(NSString *)name
                   identifer:(int)identifer
                        type:(NSString *)type;
{
    self = [self init];
    if (self) {
        self->_identifier = abs(identifer);
        if ([type isEqualToString:@"light"]) {
            self->_type = IoTZigbeeDeviceTypeLight;
        } else if ([type isEqualToString:@"thermostat"]) {
            self->_type = IoTZigbeeDeviceTypeThermostat;
        } else {
            self->_type = IoTZigbeeDeviceTypeUnknown;
        }
        if ([name length] == 0) {
            if (self->_type == IoTCoordinatorStateUnknown) {
				self->_name = NSLocalizedString(@"<Unnamed Device>", @"<Unnamed Device>");
            } else {
                self->_name = [NSString localizedStringWithFormat:@"<Unnamed %@>", [type capitalizedString]];
            }
        } else {
            self->_name = [IoTFriendlyNameMapper localizedUIRepresentationForName:name];
        }
    }
    return self;
}

- (BOOL)isEqual:(id)object
{
    if (self == object) {
        return YES;
    }

    if (![object isKindOfClass:[self class]]) {
        return NO;
    }
    return self.hash == ((IoTZigbeeDevice *)object).hash;
}

- (NSUInteger)hash {
    return self.identifier;
}

@end

#pragma mark -

#define AVSClientName @"AVS"
#define CortanaClientName @"CORTANA"
#define GVAClientName @"GVA"
#define ModularClientName @"MODULAR"


@interface IoTVoiceUIClient()
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) IoTVoiceUIClientType type;
@property (nonatomic, assign) BOOL onboarded;
@property (nonatomic, assign) BOOL wakeWordEnabled;

- (instancetype)initWithName:(NSString *)name
                   onboarded:(BOOL)onboarded
             wakeWordEnabled:(BOOL)wakeWordEnabled;

@end

@implementation IoTVoiceUIClient

+ (IoTVoiceUIClientType)clientTypeForName:(NSString *)name
{
    IoTVoiceUIClientType type = IoTVoiceUIClientTypeUnknown;
    if ([name isEqualToString:AVSClientName]) {
        type = IoTVoiceUIClientTypeAVS;
    } else if ([name isEqualToString:CortanaClientName]) {
        type = IoTVoiceUIClientTypeCortana;
    } else if ([name isEqualToString:GVAClientName]) {
        type = IoTVoiceUIClientTypeGVA;
    } else if ([name isEqualToString:ModularClientName]) {
        type = IoTVoiceUIClientTypeModular;
    }
//    NSAssert (type!=IoTVoiceUIClientTypeUnknown, @"Voice UI Name doesn't match expected.");
    return type;
}

- (instancetype)initWithName:(NSString *)name
                   onboarded:(BOOL)onboarded
             wakeWordEnabled:(BOOL)wakeWordEnabled
{
    self = [super init];
    if (self) {
        self->_name = [name copy];
        self->_type = [IoTVoiceUIClient clientTypeForName:name];
        self->_onboarded = onboarded;
        self->_wakeWordEnabled = wakeWordEnabled;
    }
    return self;
}

- (BOOL)isEqual:(id)object
{
    if (self == object) {
        return YES;
    }

    if (![object isKindOfClass:[self class]]) {
        return NO;
    }
    return self.hash == ((IoTVoiceUIClient *)object).hash;
}

- (NSUInteger)hash {
    return self.type;
}

@end

#pragma mark -

@interface IoTDevice () <IoTSysControllerObserver>  {
    __weak id<IoTDeviceUpdates> _updatesDelegate;
}

@property (atomic, assign) BOOL dispatchedAfterInitialFetch;
@property (nonatomic, weak) IoTSysController *iotSysController;
@property (nonatomic, strong) dispatch_queue_t stateSyncQueue;
@property (nonatomic, strong) dispatch_queue_t backgroundQueue;
@property (nonatomic, strong) IoTDeviceState *state;

@property (nonatomic, copy) NSString *deviceID;

@end

@implementation IoTDevice

- (instancetype)initWithIoTSysController:(IoTSysController *)iotSysController
                          stateSyncQueue:(dispatch_queue_t)syncQueue
{
    self = [super init];
    if (self) {
        [self replaceIoTSysController:iotSysController];
        self->_deviceID = [iotSysController.serviceInfo.identifier copy]; // Unlike AllPlay, IoTSys Ids do not persist across restarts so we maintain the first one we got.
        self->_stateSyncQueue = syncQueue;
        self->_backgroundQueue = dispatch_queue_create("com.qti.qualcomm.IoTDevice.backgroundQueue", DISPATCH_QUEUE_SERIAL);
        self->_state = [[IoTDeviceState alloc] initWithStateSyncQueue:syncQueue];
    }
    return self;
}

- (NSString *)description
{
    return [NSString stringWithFormat:@"Device: %@ DeviceID: %@", self.displayName, self.deviceID];
}

- (void)replaceIoTSysController:(IoTSysController *)iotSysController
{
    self.dispatchedAfterInitialFetch = NO;
    self.iotSysController = iotSysController;
    iotSysController.observer = self;
}

- (NSString *)host
{
    return self.iotSysController.serviceInfo.host;
}

- (void)updateWithCompletion:(nullable void(^)(BOOL))completion
             completionQueue:(nullable dispatch_queue_t)queue
{
    dispatch_group_t requestGroup = dispatch_group_create();

    BOOL success = NO;
    makeWeak(self)

    dispatch_group_enter(requestGroup);
    success = [self.iotSysController getSystemWithCompletion:^(IoTSystemAttr *outAttr, BOOL success) {
        makeStrongAndReturnIfNil(self);
        NSLog(@"***** getSystemWithCompletion ... %@", success ? @"SUCCESS" : @"FAILED");
        self.state.systemAttr = success ? outAttr : nil;
        dispatch_group_leave(requestGroup);
    } completionQueue: self.backgroundQueue];
    if (!success) {
        dispatch_group_leave(requestGroup);
    }

    dispatch_group_enter(requestGroup);
    success = [self.iotSysController getNetworkWithCompletion:^(IoTNetworkAttr *outAttr, BOOL success) {
        makeStrongAndReturnIfNil(self);
        NSLog(@"***** getNetworkWithCompletion ... %@", success ? @"SUCCESS" : @"FAILED");
        self.state.networkAttr = success ? outAttr : nil;
        dispatch_group_leave(requestGroup);
    } completionQueue: self.backgroundQueue];
    if (!success) {
        dispatch_group_leave(requestGroup);
    }

    dispatch_group_enter(requestGroup);
    success = [self.iotSysController getVoiceUIWithCompletion:^(IoTVoiceUIAttr *outAttr, BOOL success) {
        makeStrongAndReturnIfNil(self);
        NSLog(@"***** getVoiceUIWithCompletion ... %@", success ? @"SUCCESS" : @"FAILED");
        self.state.voiceAttr = success ? outAttr : nil;
        dispatch_group_leave(requestGroup);
    } completionQueue: self.backgroundQueue];
    if (!success) {
        dispatch_group_leave(requestGroup);
    }

    dispatch_group_enter(requestGroup);
    success = [self.iotSysController getBluetoothWithCompletion:^(IoTBluetoothAttr *outAttr, BOOL success) {
        makeStrongAndReturnIfNil(self);
        NSLog(@"***** getBluetoothWithCompletion ... %@", success ? @"SUCCESS" : @"FAILED");
        [self.state.bluetoothState updateWithAttribute:success ? outAttr : nil];
        if (success) {
            // It seems we don't get the paired devices by default
            [self.iotSysController postGetBtPairedDevicesWithCompletion:nil
                                                        completionQueue:nil];
        }
        dispatch_group_leave(requestGroup);
    } completionQueue: self.backgroundQueue];
    if (!success) {
        dispatch_group_leave(requestGroup);
    }

    dispatch_group_enter(requestGroup);
    success = [self.iotSysController getZigbeeWithCompletion:^(IoTZigbeeAttr *outAttr, BOOL success) {
        makeStrongAndReturnIfNil(self);
        NSLog(@"***** getZigbeeWithCompletion ... %@", success ? @"SUCCESS" : @"FAILED");
        [self.state.zigbeeState updateWithAttribute:success ? outAttr : nil];
        dispatch_group_leave(requestGroup);
    } completionQueue: self.backgroundQueue];
    if (!success) {
        dispatch_group_leave(requestGroup);
    }

    dispatch_group_notify(requestGroup, self.backgroundQueue, ^{
        makeStrongAndReturnIfNil(self)
        if (!self.dispatchedAfterInitialFetch && completion && queue) {
            self.dispatchedAfterInitialFetch = YES;
            dispatch_async(queue, ^{
                completion(YES);
            });
        }
    });

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), self.backgroundQueue, ^{
        makeStrongAndReturnIfNil(self)
        if (!self.dispatchedAfterInitialFetch && completion && queue) {
            self.dispatchedAfterInitialFetch = YES;
            dispatch_async(queue, ^{
                completion(NO);
            });
        }
    });
}

- (void)setUpdatesDelegate:(id<IoTDeviceUpdates>)updatesDelegate
{
    _updatesDelegate = updatesDelegate;
}

- (id<IoTDeviceUpdates>)updatesDelegate
{
    return _updatesDelegate;
}

- (NSString *)deviceID
{
    return self->_deviceID;
    // return self.iotSysController.serviceInfo.identifier;
}

#pragma mark - System

- (NSString *)displayName
{
    NSString *ret = self.state.systemAttr.deviceFriendlyName ?: @"";
	return [IoTFriendlyNameMapper localizedUIRepresentationForName:ret];
}

- (NSString *)firmwareVersion
{
    RETURN_GETTER(NSString *, systemAttr, firmwareVersion, @"")
}

- (NSString *)manufacturer
{
    RETURN_GETTER(NSString *, systemAttr, manufacturer, @"Unknown Manufacturer")
}

- (NSString *)modelNumber
{
    RETURN_GETTER(NSString *, systemAttr, model, @"Unknown Model")
}

- (BOOL)isBatteryPowerSupported
{
    return NO;
}

- (BOOL)onBatteryPower
{
    if ([self isBatteryPowerSupported]) {
		RETURN_GETTER(BOOL, systemAttr, batteryStatus.batteryPowered, NO)
    } else {
        return NO;
    }
}

- (uint)chargeLevel
{
    if ([self isBatteryPowerSupported]) {
        RETURN_GETTER(uint, systemAttr, batteryStatus.chargeLevel, 0)
    } else {
        return 0;
    }
}

- (int)timeUntilBatteryDischarged
{
    if ([self isBatteryPowerSupported]) {
        return self.state.systemAttr.batteryStatus.timeTillDischarge;
    } else {
        return 0;
    }
}

- (int)timeUntilBatteryCharged
{
    if ([self isBatteryPowerSupported]) {
        return self.state.systemAttr.batteryStatus.timeTillFullCharge;
    } else {
        return 0;
    }
}

- (void)setDisplayName:(NSString *)displayName
            completion:(nullable IoTCompletionHandler)completion
       completionQueue:(nullable dispatch_queue_t)queue
{
    NSString *corrected = [IoTFriendlyNameMapper dutRepresentationForLocalizedName:displayName];
    SETTER(systemAttr, deviceFriendlyName, corrected, postSystemWith)
}

#pragma mark - Network

- (BOOL)networkSupported
{
    return self.state.networkAttr != nil;
}

- (IoTNetworkInterface)networkInterface
{
    if (!self.state.networkAttr) {
        return IoTNetworkInterfaceNone;
    }
    if (self.state.networkAttr.ethernetAdapter.connectedState) {
        return IoTNetworkInterfaceEthernet;
    }
    if (self.state.networkAttr.wifiAdapter.connectedState) {
        return IoTNetworkInterfaceWiFi;
    }
    return IoTNetworkInterfaceNone;
}

- (NSString *)wifiIPAddress
{
    RETURN_GETTER(NSString *, networkAttr.wifiAdapter, iPAddress, @"")
}

- (NSString *)wifiMacAddress
{
    RETURN_GETTER(NSString *, networkAttr.wifiAdapter, macAddress, @"")
}

- (NSString *)ethernetIPAddress
{
    RETURN_GETTER(NSString *, networkAttr.ethernetAdapter, iPAddress, @"")
}

- (NSString *)ethernetMacAddress
{
    RETURN_GETTER(NSString *, networkAttr.ethernetAdapter, macAddress, @"")
}

- (NSString *)wifiSSID
{
    RETURN_GETTER(NSString *, networkAttr.accessPoint, sSID, @"")
}

#define RSSI_MIN -100.0
#define RSSI_MAX -55.0
- (uint)wifiQuality:(uint)numberOfBands
{
    if (self.state.networkAttr) {
        int rssi = [self.state.networkAttr.accessPoint.rSSI intValue];
        if (rssi <= RSSI_MIN) {
            return 0;
        } else if (rssi >= RSSI_MAX) {
            return numberOfBands - 1;
        } else {
            float inputRange = RSSI_MAX - RSSI_MIN;
            float outputRange = numberOfBands - 1;
            return (uint)((float)(rssi - RSSI_MIN) * outputRange / inputRange);
        }
    } else {
        return 0;
    }
}

- (void)updateNetworkInfoWithCompletion:(nullable IoTCompletionHandler)completion
                        completionQueue:(nullable dispatch_queue_t)queue
{
    makeWeak(self)
    BOOL success = [self.iotSysController getNetworkWithCompletion:^(IoTNetworkAttr *outAttr, BOOL success) {
        makeStrongAndReturnIfNil(self);
        // self.state.networkAttr = success ? outAttr : nil;
        // We don't set network attr here as it gets updated as a notification.
        if (queue && completion) {
            dispatch_async(queue, ^{
                completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    } completionQueue: self.backgroundQueue];
    if (!success) {
        if (queue && completion) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
}

#pragma mark - Voice UI

- (BOOL)voiceUISupported
{
    return self.state.voiceAttr != nil;
}

- (BOOL)voiceUIEnabled
{
	RETURN_GETTER(BOOL, voiceAttr, enableVoiceUI, NO)
}

- (NSArray <IoTVoiceUIClient *> *)voiceUIClients
{
    NSArray<IoTVoiceUIClientAttr *> *clientAttrs = self.state.voiceAttr.voiceUIClients;
    NSMutableArray<IoTVoiceUIClient *> *clients = [NSMutableArray arrayWithCapacity:clientAttrs.count];

    for (IoTVoiceUIClientAttr *clientAttr in clientAttrs) {
        IoTVoiceUIClientType type = [IoTVoiceUIClient clientTypeForName:clientAttr.name];
        if (type != IoTVoiceUIClientTypeUnknown) {
            BOOL onboarded = NO;
            BOOL wakewordEnabled = clientAttr.wakewordStatus;
            switch (type) {
                case IoTVoiceUIClientTypeAVS:
                    onboarded = self.state.voiceAttr.aVSOnboarded;
                    break;
                case IoTVoiceUIClientTypeCortana:
                    // TODO: Implement onboarded flag for services other than AVS.
                    onboarded = NO;
                    break;
                case IoTVoiceUIClientTypeGVA:
                    onboarded = NO;
                    break;
                case IoTVoiceUIClientTypeModular:
                    onboarded = YES;
                    break;
                default:
                    break;
            }

            IoTVoiceUIClient *client = [[IoTVoiceUIClient alloc] initWithName:clientAttr.name
                                                                    onboarded:onboarded
                                                              wakeWordEnabled:wakewordEnabled];

            [clients addObject:client];
        }
    }
    return [clients copy];
}

- (IoTVoiceUIClient *)defaultVoiceUIClient
{
    NSArray <IoTVoiceUIClient *> *clients = [self voiceUIClients];
    NSString *defaultClientName = self.state.voiceAttr.defaultVoiceUIClient;

    for (IoTVoiceUIClient *client in clients) {
        if ([client.name isEqualToString:defaultClientName]) {
            return client;
        }
    }
    return [clients firstObject];
}

- (void)setDefaultVoiceUIClient:(IoTVoiceUIClient *)client
                     completion:(nullable IoTCompletionHandler)completion
                completionQueue:(nullable dispatch_queue_t)queue
{
    IoTVoiceUIAttr *attr = [self.state.voiceAttr copy];
    attr.defaultVoiceUIClient = client.name;

    makeWeak(self)
    BOOL success = [self.iotSysController postVoiceUIWith:attr
                                               completion:^(BOOL success) {
                                                   makeStrongAndReturnIfNil(self);
                                                   if (queue && completion) {
                                                       dispatch_async(queue, ^{
                                                           completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                                                       });
                                                   }
                                               } completionQueue: self.backgroundQueue];
    if (!success) {
        if (queue && completion) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
}

- (void)setVoiceUIClient:(IoTVoiceUIClient *)client
         wakeWordEnabled:(BOOL)wakeWordEnabled
              completion:(nullable IoTCompletionHandler)completion
         completionQueue:(nullable dispatch_queue_t)queue
{
    IoTSelectClientInAttr *attr = [[IoTSelectClientInAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
    attr.wakewordStatus = wakeWordEnabled;
    attr.client = client.name;

    makeWeak(self)
    BOOL success = [self.iotSysController postEnableVoiceUIClientWith:attr
                                                           completion:^(BOOL success) {
                                                               makeStrongAndReturnIfNil(self);
                                                               if (queue && completion) {
                                                                   dispatch_async(queue, ^{
                                                                       completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                                                                   });
                                                               }
                                                           } completionQueue: self.backgroundQueue];
    if (!success) {
        if (queue && completion) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
}

- (void)setVoiceUIEnabled:(BOOL)enabled
               completion:(nullable IoTCompletionHandler)completion
          completionQueue:(nullable dispatch_queue_t)queue
{
    IoTVoiceUIAttr *attr = [self.state.voiceAttr copy];
    attr.enableVoiceUI = enabled;

    makeWeak(self)
    BOOL success = [self.iotSysController postVoiceUIWith:attr
                                               completion:^(BOOL success) {
                                                   makeStrongAndReturnIfNil(self);
                                                   if (queue && completion) {
                                                       dispatch_async(queue, ^{
                                                           completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                                                       });
                                                   }
                                               } completionQueue: self.backgroundQueue];
    if (!success) {
        if (queue && completion) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
}

- (NSArray<NSString *> *)modularClientLanguages
{
    RETURN_GETTER(NSArray<NSString *> *, voiceAttr, modularClientLanguages, @[])
}

- (NSString *)selectedModularClientLanguages
{
    RETURN_GETTER(NSString *, voiceAttr, selectedModularClientLanguage, [[self modularClientLanguages] firstObject])
}


- (NSString *)modularClientAlertFile
{
//	RETURN_GETTER(NSString *, voiceAttr, modularClientAlertFile, @"")
    return @""; // Not in scope for this release.
}

- (void)setSelectedModularClientLanguage:(NSString *)language
                              completion:(nullable IoTCompletionHandler)completion
                         completionQueue:(nullable dispatch_queue_t)queue
{
    IoTVoiceUIAttr *attr = [self.state.voiceAttr copy];
    attr.selectedModularClientLanguage = language;

    makeWeak(self)
    BOOL success = [self.iotSysController postVoiceUIWith:attr
                                               completion:^(BOOL success) {
                                                   makeStrongAndReturnIfNil(self);
                                                   if (queue && completion) {
                                                       dispatch_async(queue, ^{
                                                           completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                                                       });
                                                   }
                                               } completionQueue: self.backgroundQueue];
    if (!success) {
        if (queue && completion) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
}

- (void)setModularClientAlertFile:(NSString *)file
                       completion:(nullable IoTCompletionHandler)completion
                  completionQueue:(nullable dispatch_queue_t)queue
{
    // Not in scope for this release
/*
    IoTVoiceUIAttr *attr = [self.state.voiceAttr copy];
    attr.modularClientAlertFile = file;

    makeWeak(self)
    BOOL success = [self.iotSysController postVoiceUIWith:attr
                                               completion:^(BOOL success) {
                                                   makeStrongAndReturnIfNil(self);
                                                   if (queue && completion) {
                                                       dispatch_async(queue, ^{
                                                           completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                                                       });
                                                   }
                                               } completionQueue: self.backgroundQueue];
    if (!success) {
        if (queue && completion) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
*/
}

// AVS

- (NSString *)selectedAVSLocale
{
    RETURN_GETTER(NSString *, voiceAttr, aVSLocale, @"")
}

- (NSArray<NSString *> *)avsLocales
{
    RETURN_GETTER(NSArray<NSString *> *, voiceAttr, aVSLocaleList, @[])
}

- (void)setSelectedAVSLocale:(NSString *)locale
                  completion:(nullable IoTCompletionHandler)completion
             completionQueue:(nullable dispatch_queue_t)queue
{
    IoTVoiceUIAttr *attr = [self.state.voiceAttr copy];
    attr.aVSLocale = locale;

    makeWeak(self)
    BOOL success = [self.iotSysController postVoiceUIWith:attr
                                               completion:^(BOOL success) {
                                                   makeStrongAndReturnIfNil(self);
                                                   if (queue && completion) {
                                                       dispatch_async(queue, ^{
                                                           completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                                                       });
                                                   }
                                               } completionQueue: self.backgroundQueue];
    if (!success) {
        if (queue && completion) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
}

- (void)startAVSOnboardingWithCompletion:(nullable IoTCompletionHandler)completion
                         completionQueue:(nullable dispatch_queue_t)queue
{
	ACTION(postStartAVSOnboardingWithCompletion)
}

- (void)removeAVSCredentialWithCompletion:(nullable IoTCompletionHandler)completion
                          completionQueue:(nullable dispatch_queue_t)queue
{
    IoTClientInAttr *client = [[IoTClientInAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
    client.client = AVSClientName;

    makeWeak(self)
    BOOL success = [self.iotSysController postDeleteCredentialWith:client
                                                        completion:^(BOOL success) {
                                                            makeStrongAndReturnIfNil(self);
                                                            if (queue && completion) {
                                                                dispatch_async(queue, ^{
                                                                    completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                                                                });
                                                            }
                                                        } completionQueue: self.backgroundQueue];
    if (!success) {
        if (queue && completion) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
}

#pragma mark - Bluetooth

- (BOOL)bluetoothSupported
{
    RETURN_GETTER(BOOL, bluetoothState, supported, NO)
}

- (IoTBluetoothAdapterState)bluetoothEnabled
{
    RETURN_GETTER(IoTBluetoothAdapterState, bluetoothState, adapterState, IoTBluetoothAdapterStateUnknown)
}

- (IoTDiscoverable)bluetoothDiscoverable
{
	RETURN_GETTER(IoTDiscoverable, bluetoothState, discoverable, IoTDiscoverableUnknown)
}

- (IoTBluetoothDevice *)connectedBluetoothDevice
{
    NSString *address = self.state.bluetoothState.connectedBTDeviceAddress;
    if (!address) {
        return nil;
    }
    NSArray<IoTBluetoothDevice *> *pairedDevices = self.pairedBluetoothDevices;
    __block IoTBluetoothDevice *found = nil;

    [pairedDevices enumerateObjectsUsingBlock:^(IoTBluetoothDevice * _Nonnull device, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([device.address isEqualToString:address]) {
            found = device;
            *stop = YES;
        }
    }];

    return found ?: [[IoTBluetoothDevice alloc] initWithName: NSLocalizedString(@"Unknown", @"Unknown Device")
                                                     address: address];
}

- (NSArray<IoTBluetoothDevice *> *)pairedBluetoothDevices
{
    if (self.state.bluetoothState.adapterState == IoTBluetoothAdapterStateDisabled) {
        return @[];
    }

    NSArray<IoTBtDeviceStatusAttr *> *pairedAttrs = self.state.bluetoothState.pairedDevices ?: @[];
    NSMutableArray<IoTBluetoothDevice *> *devices = [NSMutableArray arrayWithCapacity:pairedAttrs.count];
    for (IoTBtDeviceStatusAttr *attr in pairedAttrs) {
        if (attr.name.length > 0 || attr.address.length > 0) {
            IoTBluetoothDevice *device = [[IoTBluetoothDevice alloc] initWithName:attr.name ?: @""
                                                                          address:attr.address ?: @""];
            [devices addObject:device];
        }
    }
    return [devices copy];
}

- (BOOL)isBluetoothScanning
{
	RETURN_GETTER(BOOL, bluetoothState, scanning, NO)
}

- (void)startUpdateBtAdapterStateWithCompletion:(nullable IoTCompletionHandler)completion
                                completionQueue:(nullable dispatch_queue_t)queue
{
    ACTION(postGetBtAdapterStateWithCompletion)
}

- (void)setBtAdapterEnabled:(BOOL)enabled
                 completion:(nullable IoTCompletionHandler)completion
            completionQueue:(nullable dispatch_queue_t)queue
{
    IoTEnableBtAdapterInAttr *attr = [[IoTEnableBtAdapterInAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
    attr.adapterState = enabled;

    makeWeak(self)
    BOOL success = [self.iotSysController postEnableBtAdapterWith:attr
                                                       completion:^(BOOL success) {
        makeStrongAndReturnIfNil(self);
        if (queue && completion) {
            dispatch_async(queue, ^{
                completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    } completionQueue: self.backgroundQueue];
    if (!success) {
        if (queue && completion) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
}

- (void)startBtDiscoveryModeWithTimeout:(UInt32)seconds
                             completion:(nullable IoTCompletionHandler)completion
                        completionQueue:(nullable dispatch_queue_t)queue
{
    IoTTimeoutSecInAttr *attr = [[IoTTimeoutSecInAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
    attr.timeoutSecs = seconds;

    makeWeak(self)
    BOOL success = [self.iotSysController postEnterBtDiscoveryModeWith:attr
                                                            completion:^(BOOL success) {
                                                           makeStrongAndReturnIfNil(self);
                                                           if (queue && completion) {
                                                               dispatch_async(queue, ^{
                                                                   completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                                                               });
                                                           }
                                                       } completionQueue: self.backgroundQueue];
    if (!success) {
        if (queue && completion) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
}

- (void)exitBtDiscoveryModeWithCompletion:(nullable IoTCompletionHandler)completion
                          completionQueue:(nullable dispatch_queue_t)queue
{
    ACTION(postCancelBtDiscoveryModeWithCompletion)
}

- (void)startBtScanWithCompletion:(nullable IoTCompletionHandler)completion
                  completionQueue:(nullable dispatch_queue_t)queue
{
    ACTION(postStartBtScanWithCompletion)
}

- (void)stopBtScanWithCompletion:(nullable IoTCompletionHandler)completion
                 completionQueue:(nullable dispatch_queue_t)queue
{
    ACTION(postStopBtScanWithCompletion)
}

- (void)startConnectBtDevice:(IoTBluetoothDevice *)device
                  completion:(nullable IoTCompletionHandler)completion
             completionQueue:(nullable dispatch_queue_t)queue
{
    IoTAddressInAttr *attr = [[IoTAddressInAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
    attr.address = device.address;

    makeWeak(self)
    BOOL success = [self.iotSysController postConnectBtDeviceWith:attr
                                                       completion:^(BOOL success) {
                                                           makeStrongAndReturnIfNil(self);
                                                           if (queue && completion) {
                                                               dispatch_async(queue, ^{
                                                                   completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                                                               });
                                                           }
                                                       } completionQueue: self.backgroundQueue];
    if (!success) {
        if (queue && completion) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
}

- (void)startDisconnectBtDevice:(IoTBluetoothDevice *)device
                     completion:(nullable IoTCompletionHandler)completion
                completionQueue:(nullable dispatch_queue_t)queue
{
    IoTAddressInAttr *attr = [[IoTAddressInAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
    attr.address = device.address;

    makeWeak(self)
    BOOL success = [self.iotSysController postDisconnectBtDeviceWith:attr
                                                          completion:^(BOOL success) {
                                                              makeStrongAndReturnIfNil(self);
                                                              if (queue && completion) {
                                                                  dispatch_async(queue, ^{
                                                                      completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                                                                  });
                                                              }
                                                          } completionQueue: self.backgroundQueue];
    if (!success) {
        if (queue && completion) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
}

- (void)startClearPairedBtDevicesWithCompletion:(nullable IoTCompletionHandler)completion
                                completionQueue:(nullable dispatch_queue_t)queue
{
    ACTION(postClearBtPairedListWithCompletion)
}

- (void)startUpdatePairedBtDevicesWithCompletion:(nullable IoTCompletionHandler)completion
                                 completionQueue:(nullable dispatch_queue_t)queue
{
    ACTION(postGetBtPairedDevicesWithCompletion)
}

- (void)startPairBtDevice:(IoTBluetoothDevice *)device
               completion:(nullable IoTCompletionHandler)completion
          completionQueue:(nullable dispatch_queue_t)queue
{
    IoTAddressInAttr *attr = [[IoTAddressInAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
    attr.address = device.address;

    makeWeak(self)
    BOOL success = [self.iotSysController postPairBtDeviceWith:attr
                                                    completion:^(BOOL success) {
                                                        makeStrongAndReturnIfNil(self);
                                                        if (queue && completion) {
                                                            dispatch_async(queue, ^{
                                                                completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                                                            });
                                                        }
                                                    } completionQueue: self.backgroundQueue];
    if (!success) {
        if (queue && completion) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
}

- (void)startUnpairBtDevice:(IoTBluetoothDevice *)device
                 completion:(nullable IoTCompletionHandler)completion
            completionQueue:(nullable dispatch_queue_t)queue
{
    IoTAddressInAttr *attr = [[IoTAddressInAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
    attr.address = device.address;

    makeWeak(self)
    BOOL success = [self.iotSysController postUnpairBtDeviceWith:attr
                                                      completion:^(BOOL success) {
                                                          makeStrongAndReturnIfNil(self);
                                                          if (queue && completion) {
                                                              dispatch_async(queue, ^{
                                                                  completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                                                              });
                                                          }
                                                      } completionQueue: self.backgroundQueue];
    if (!success) {
        if (queue && completion) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
}

#pragma mark - Zigbee

- (BOOL)zigbeeSupported
{
    return self.state.zigbeeState.supported;
}

- (IoTZigbeeAdapterState)zigbeeEnabled
{
    RETURN_GETTER(IoTZigbeeAdapterState, zigbeeState, adapterState, IoTZigbeeAdapterStateUnknown)
}

- (IoTCoordinatorState)zigbeeCoordinatorState
{
    RETURN_GETTER(BOOL, zigbeeState, coordinatorState, IoTCoordinatorStateUnknown)
}

- (NSArray<IoTZigbeeDevice *> *)zigbeeDevices
{
    NSArray<IoTZbDeviceAttr *> *joinedAttrs = self.state.zigbeeState.joinedDevices ?: @[];
    NSMutableArray<IoTZigbeeDevice *> *devices = [NSMutableArray arrayWithCapacity:joinedAttrs.count];
    for (IoTZbDeviceAttr *attr in joinedAttrs) {
        IoTZigbeeDevice *device = [[IoTZigbeeDevice alloc] initWithName:attr.friendlyName
                                                              identifer:attr.deviceIdentifier
                                                                   type:attr.deviceType];
        [devices addObject:device];
    }
    return [devices copy];
}

- (void)setZbAdapterEnabled:(BOOL)enabled
                 completion:(nullable IoTCompletionHandler)completion
            completionQueue:(nullable dispatch_queue_t)queue
{
    IoTEnableZbAdapterInAttr *attr = [[IoTEnableZbAdapterInAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
    attr.adapterState = enabled;

    makeWeak(self)
    BOOL success = [self.iotSysController postEnableZbAdapterWith:attr
                                                       completion:^(BOOL success) {
                                                           makeStrongAndReturnIfNil(self);
                                                           if (queue && completion) {
                                                               dispatch_async(queue, ^{
                                                                   completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                                                               });
                                                           }
                                                       } completionQueue: self.backgroundQueue];
    if (!success) {
        if (queue && completion) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
}

// Form ZB Network implies become Co-ordinator.
- (void)startFormZbNetworkWithCompletion:(nullable IoTCompletionHandler)completion
                         completionQueue:(nullable dispatch_queue_t)queue
{
    makeWeak(self)
    if ([self zigbeeCoordinatorState] == IoTCoordinatorStateNominated) {
        if (queue && completion) {
            dispatch_async(queue, ^{
                makeStrongAndReturnIfNil(self);
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
        return;
    }

    BOOL success = [self.iotSysController postFormZbNetworkWithCompletion:^(BOOL success) {
        makeStrongAndReturnIfNil(self);
        if (queue && completion) {
            dispatch_async(queue, ^{
                completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    } completionQueue: self.backgroundQueue];

    if (!success) {
        if (queue && completion) {
            makeStrongAndReturnIfNil(self);
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
}

// Use Start ZB Joining when already coordinator.
- (void)startZbJoiningWithTimeout:(UInt32)seconds
                       completion:(nullable IoTCompletionHandler)completion
                  completionQueue:(nullable dispatch_queue_t)queue
{
    makeWeak(self)
    if ([self zigbeeCoordinatorState] != IoTCoordinatorStateNominated) {
        if (queue && completion) {
            dispatch_async(queue, ^{
                makeStrongAndReturnIfNil(self);
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
        return;
    }
    
    IoTTimeoutSecInAttr *attr = [[IoTTimeoutSecInAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
    attr.timeoutSecs = seconds;
    
    BOOL success = [self.iotSysController postStartZbJoiningWith:attr
                                                      completion:^(BOOL success) {
                                                          makeStrongAndReturnIfNil(self);
                                                          if (queue && completion) {
                                                              dispatch_async(queue, ^{
                                                                  completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                                                              });
                                                          }
                                                      } completionQueue: self.backgroundQueue];
    if (!success) {
        if (queue && completion) {
            makeStrongAndReturnIfNil(self);
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
}


- (void)startSetZbDeviceName:(NSString *)name
                   identifer:(NSUInteger)identifier
                  completion:(nullable IoTCompletionHandler)completion
             completionQueue:(nullable dispatch_queue_t)queue
{
    makeWeak(self)
    if ([self zigbeeCoordinatorState] != IoTCoordinatorStateNominated) {
        if (queue && completion) {
            dispatch_async(queue, ^{
                makeStrongAndReturnIfNil(self);
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
        return;
    }

    IoTSetZbFriendlyNameInAttr *attr = [[IoTSetZbFriendlyNameInAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];

    NSString *corrected = [IoTFriendlyNameMapper dutRepresentationForLocalizedName:name];
    attr.friendlyName = corrected;
    attr.deviceIdentifier = (int)identifier;
    
    BOOL success = [self.iotSysController postSetZbFriendlyNameWith:attr
                                                         completion:^(BOOL success) {
                                                             makeStrongAndReturnIfNil(self);
                                                             if (queue && completion) {
                                                                 dispatch_async(queue, ^{
                                                                     completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                                                                 });
                                                             }
                                                         } completionQueue: self.backgroundQueue];
    if (!success) {
        if (queue && completion) {
            makeStrongAndReturnIfNil(self);
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
}

#pragma mark - Actions

- (void)rebootWithCompletion:(nullable IoTCompletionHandler)completion
             completionQueue:(nullable dispatch_queue_t)queue
{
    ACTION(postRestartWithCompletion)
}

- (void)factoryResetWithCompletion:(nullable IoTCompletionHandler)completion
                   completionQueue:(nullable dispatch_queue_t)queue
{
    ACTION(postFactoryResetWithCompletion)
}

- (BOOL)networkInterfaceChanged:(IoTNetworkInterfaceAttr *)newAttr
                       oldState:(IoTNetworkInterfaceAttr *)oldAttr
{
    BOOL equal = newAttr.connectedState == oldAttr.connectedState &&
    	[newAttr.iPAddress isEqualToString:oldAttr.iPAddress] &&
    	[newAttr.macAddress isEqualToString:oldAttr.macAddress];

    return !equal;
}

- (void)iotSysController:(IoTSysController *)controller
        didObserveChange:(IoTSysObserveOrigin)type
   wrappedRepresentation:(IoTWrappedRepresentation *)wrapped {
    NSArray<NSString *> *keys = [wrapped keys];
    
    BOOL canSend = self.dispatchedAfterInitialFetch; // We only notify if we have finished all fetches - this should always be true.
    if (canSend) {
        NSLog(@"**** Got observe callback for IoTSys:  %ld\nkeys: %@", (long)type, keys);
    } else {
		NSLog(@"**** Ignoring observe callback for IoTSys:  %ld\nkeys: %@", (long)type, keys);
    }
    switch (type) {
        case IoTSysObserveOriginBluetooth:
        {
            // We determine what happened by looking at the changed keys.
            NSString *previousConnectedDeviceAddress = self.state.bluetoothState.connectedBTDeviceAddress;
            BOOL previousScanState = self.state.bluetoothState.scanning;
            keys = [self.state.bluetoothState updateWithWrapper:wrapped];
            if (previousScanState != self.state.bluetoothState.scanning) {
                [self.updatesDelegate device:self btScanStateDidChange:self.state.bluetoothState.scanning];
            }
            if (canSend) {
                if ([keys containsObject:[NSString stringWithUTF8String:kAdapterState]]) {
                    [self.updatesDelegate device:self btAdapterStateDidChange:self.bluetoothEnabled];
                    if (self.state.bluetoothState.adapterState != IoTBluetoothAdapterStateDisabled) {
                        // It seems we don't get the paired devices by default
                        [self.iotSysController postGetBtPairedDevicesWithCompletion:nil
                                                                    completionQueue:nil];
                    }
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kDiscoverable]]) {
                    [self.updatesDelegate device:self btDiscoverabilityDidChange:self.bluetoothDiscoverable];
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kConnectedState]]) {
                    // It seems we don't get the paired devices by default
                    [self.iotSysController postGetBtPairedDevicesWithCompletion:nil
                                                                completionQueue:nil];

                    if (self.state.bluetoothState.connectedBTDeviceAddress) {
                        // Connection?
                        if (previousConnectedDeviceAddress &&
                            ![previousConnectedDeviceAddress isEqualToString:self.state.bluetoothState.connectedBTDeviceAddress]) {
                            IoTBluetoothDevice *btDevice = [[IoTBluetoothDevice alloc] initWithName: @"" address:previousConnectedDeviceAddress];
                            [self.updatesDelegate device:self btDidDisconnectDevice:btDevice];
                        }
                        [self.updatesDelegate device:self btDidConnectDevice:self.connectedBluetoothDevice];
                    } else {
                        // Disconnection
                        if (previousConnectedDeviceAddress) {
                            IoTBluetoothDevice *btDevice = [[IoTBluetoothDevice alloc] initWithName: @"" address:previousConnectedDeviceAddress];
                            [self.updatesDelegate device:self btDidDisconnectDevice:btDevice];
                        }
                    }
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kScanResult]]) {
                    IoTBluetoothDevice *btDevice = [[IoTBluetoothDevice alloc] initWithName:self.state.bluetoothState.discoveredDevice.name ?: @""
                                                                                    address:self.state.bluetoothState.discoveredDevice.address ?: @""];
                    if (!previousScanState) {
                        [self.iotSysController postGetBtPairedDevicesWithCompletion:nil
                                                                    completionQueue:nil];
                    }
                    [self.updatesDelegate device:self btScanDidDiscoverBtDevice:btDevice];
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kPairedDevices]]) {
                    [self.updatesDelegate device:self btPairedDevicesDidChange:self.pairedBluetoothDevices];
                }

                if ([keys containsObject:[NSString stringWithUTF8String:kPairedState]]) {
                    [self.iotSysController postGetBtPairedDevicesWithCompletion:nil
                                                                completionQueue:nil];
                }
            }
            break;
        }
        case IoTSysObserveOriginNetwork: {
            if (!self.state.networkAttr) {
                self.state.networkAttr = [[IoTNetworkAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
            }
            IoTNetworkAttr *oldState = [self.state.networkAttr copy];
            [self.state.networkAttr unpack:wrapped];
            if (canSend) {
                if ([keys containsObject:[NSString stringWithUTF8String:kEthernetAdapter]] &&
                    [self networkInterfaceChanged:self.state.networkAttr.ethernetAdapter oldState:oldState.ethernetAdapter]) {
                    [self.updatesDelegate deviceDidChangeEthernetState:self];
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kWifiAdapter]] &&
                    [self networkInterfaceChanged:self.state.networkAttr.wifiAdapter oldState:oldState.wifiAdapter]) {
                    [self.updatesDelegate deviceDidChangeWiFiState:self];
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kAccessPoint]]) {
                    [self.updatesDelegate deviceDidChangeAccessPointState:self];
                }
            }
            break;
        }
        case IoTSysObserveOriginSystem:
            if (!self.state.systemAttr) {
                self.state.systemAttr = [[IoTSystemAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
            }
            [self.state.systemAttr unpack:wrapped];
            if (canSend) {
                if ([keys containsObject:[NSString stringWithUTF8String:kBatteryStatus]]) {
                    [self.updatesDelegate deviceDidChangeBatteryState:self];
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kDeviceFriendlyName]]) {
                    [self.updatesDelegate device:self didChangeName:self.displayName];
                }
            }
            break;
        case IoTSysObserveOriginVoiceUI:
            if (!self.state.voiceAttr) {
                self.state.voiceAttr = [[IoTVoiceUIAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
            }
            [self.state.voiceAttr unpack:wrapped];
            if (canSend) {
                if (([keys containsObject:[NSString stringWithUTF8String:kVoiceUIClients]]) ||
                    ([keys containsObject:[NSString stringWithUTF8String:kAVSOnboarded]]) ||
                    ([keys containsObject:[NSString stringWithUTF8String:kAVSLocale]]) ||
 //                   ([keys containsObject:[NSString stringWithUTF8String:kModularClientAlertFile]]) ||
                    ([keys containsObject:[NSString stringWithUTF8String:kSelectedModularClientLanguage]])) {
                    [self.updatesDelegate device:self voiceUIClientsDidChange:self.voiceUIClients];
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kDefaultVoiceUIClient]]) {
                    [self.updatesDelegate device:self voiceUIDefaultClientDidChange:self.defaultVoiceUIClient];
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kEnableVoiceUI]]) {
                    [self.updatesDelegate device:self voiceUIEnabledStateDidChange:self.voiceUIEnabled];
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kAuthenticateAVS]]) {
                    [self.updatesDelegate device:self
          voiceUIDidProvideAVSAuthenticationCode:self.state.voiceAttr.authenticateAVS.code
                                             url:self.state.voiceAttr.authenticateAVS.url];
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kAVSOnboardingError]]) {
                    [self.updatesDelegate device:self
            voiceUIOnboardingDidErrorWithTimeout:self.state.voiceAttr.aVSOnboardingError.errorReason == IoTErrorReasonTimedout
                                       reattempt:self.state.voiceAttr.aVSOnboardingError.reattempt];
                }
            }
            break;
        case IoTSysObserveOriginZigbee:
            keys = [self.state.zigbeeState updateWithWrapper:wrapped];
            if (canSend) {
                if ([keys containsObject:[NSString stringWithUTF8String:kAdapterState]]) {
                    [self.updatesDelegate device:self zbAdapterStateDidChange:self.zigbeeEnabled];
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kCoordinatorState]]) {
                    [self.updatesDelegate device:self zbCoordinatorStateDidChange:self.zigbeeCoordinatorState];
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kJoinedDevices]]) {
                    [self.updatesDelegate device:self zbJoinedDevicesDidChange:self.zigbeeDevices];
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kJoiningState]]) {
                    if (self.state.zigbeeState.joiningState == IoTJoiningStateAllowed) {
                        [self.updatesDelegate device:self zbJoiningStateDidChange:YES];
                    } else {
                        [self.updatesDelegate device:self zbJoiningStateDidChange:NO];
                    }
                }
            }
            break;
        default:
            break;
            
    }
}

@end
