def __init__(self, event_callback=None, persistence=False, persistence_file='mysensors.pickle', protocol_version='1.4'): """Setup Gateway.""" self.queue = Queue() self.lock = threading.Lock() self.event_callback = event_callback self.sensors = {} self.metric = True # if true - use metric, if false - use imperial self.debug = False # if true - print all received messages self.persistence = persistence # if true - save sensors to disk self.persistence_file = persistence_file # path to persistence file self.persistence_bak = '{}.bak'.format(self.persistence_file) if persistence: self._safe_load_sensors() self.protocol_version = float(protocol_version) if 1.5 <= self.protocol_version < 2.0: _const = import_module('mysensors.const_15') elif self.protocol_version >= 2.0: _const = import_module('mysensors.const_20') else: _const = import_module('mysensors.const_14') self.const = _const self.ota = OTAFirmware(self.sensors, self.const)
def __init__(self, event_callback=None, persistence=False, persistence_file='mysensors.pickle', protocol_version='1.4'): """Setup Gateway.""" self.queue = Queue() self.lock = threading.Lock() self.event_callback = event_callback self.sensors = {} self.metric = True # if true - use metric, if false - use imperial self.debug = False # if true - print all received messages self.persistence = persistence # if true - save sensors to disk self.persistence_file = persistence_file # path to persistence file self.persistence_bak = '{}.bak'.format(self.persistence_file) self.protocol_version = float(protocol_version) if 1.5 <= self.protocol_version < 2.0: _const = import_module('mysensors.const_15') elif self.protocol_version >= 2.0: _const = import_module('mysensors.const_20') else: _const = import_module('mysensors.const_14') self.const = _const self.ota = OTAFirmware(self.sensors, self.const) if persistence: self._safe_load_sensors()
class Gateway(object): """Base implementation for a MySensors Gateway.""" # pylint: disable=too-many-instance-attributes def __init__(self, event_callback=None, persistence=False, persistence_file='mysensors.pickle', protocol_version='1.4'): """Setup Gateway.""" self.queue = Queue() self.lock = threading.Lock() self.event_callback = event_callback self.sensors = {} self.metric = True # if true - use metric, if false - use imperial self.debug = False # if true - print all received messages self.persistence = persistence # if true - save sensors to disk self.persistence_file = persistence_file # path to persistence file self.persistence_bak = '{}.bak'.format(self.persistence_file) if persistence: self._safe_load_sensors() self.protocol_version = float(protocol_version) if 1.5 <= self.protocol_version < 2.0: _const = import_module('mysensors.const_15') elif self.protocol_version >= 2.0: _const = import_module('mysensors.const_20') else: _const = import_module('mysensors.const_14') self.const = _const self.ota = OTAFirmware(self.sensors, self.const) def _handle_presentation(self, msg): """Process a presentation message.""" if msg.child_id == 255: # this is a presentation of the sensor platform sensorid = self.add_sensor(msg.node_id) self.sensors[msg.node_id].type = msg.sub_type self.sensors[msg.node_id].protocol_version = msg.payload self.sensors[msg.node_id].reboot = False self.alert(msg.node_id) return sensorid if sensorid is not None else None else: # this is a presentation of a child sensor if not self.is_sensor(msg.node_id): _LOGGER.error('Node %s is unknown, will not add child %s.', msg.node_id, msg.child_id) return child_id = self.sensors[msg.node_id].add_child_sensor( msg.child_id, msg.sub_type, msg.payload) self.alert(msg.node_id) return child_id if child_id is not None else None def _handle_set(self, msg): """Process a set message.""" if not self.is_sensor(msg.node_id, msg.child_id): return self.sensors[msg.node_id].set_child_value(msg.child_id, msg.sub_type, msg.payload) if self.sensors[msg.node_id].new_state: self.sensors[msg.node_id].set_child_value( msg.child_id, msg.sub_type, msg.payload, children=self.sensors[msg.node_id].new_state) self.alert(msg.node_id) # Check if reboot is true if self.sensors[msg.node_id].reboot: return msg.copy(child_id=255, type=self.const.MessageType.internal, ack=0, sub_type=self.const.Internal.I_REBOOT, payload='') def _handle_req(self, msg): """Process a req message. This will return the value if it exists. If no value exists, nothing is returned. """ if self.is_sensor(msg.node_id, msg.child_id): value = self.sensors[msg.node_id].children[ msg.child_id].values.get(msg.sub_type) if value is not None: return msg.copy(type=self.const.MessageType.set, payload=value) def _handle_heartbeat(self, msg): """Process a heartbeat message.""" if not self.is_sensor(msg.node_id): return while self.sensors[msg.node_id].queue: self.fill_queue(str, (self.sensors[msg.node_id].queue.popleft(), )) for child in self.sensors[msg.node_id].children.values(): new_child = self.sensors[msg.node_id].new_state.get( child.id, ChildSensor(child.id, child.type, child.description)) self.sensors[msg.node_id].new_state[child.id] = new_child for value_type, value in child.values.items(): new_value = new_child.values.get(value_type) if new_value is not None and new_value != value: self.fill_queue(self.sensors[msg.node_id].set_child_value, (child.id, value_type, new_value)) def _handle_internal(self, msg): """Process an internal protocol message.""" if msg.sub_type == self.const.Internal.I_ID_REQUEST: node_id = self.add_sensor() return msg.copy(ack=0, sub_type=self.const.Internal.I_ID_RESPONSE, payload=node_id) if node_id is not None else None elif msg.sub_type == self.const.Internal.I_SKETCH_NAME: if self.is_sensor(msg.node_id): self.sensors[msg.node_id].sketch_name = msg.payload self.alert(msg.node_id) elif msg.sub_type == self.const.Internal.I_SKETCH_VERSION: if self.is_sensor(msg.node_id): self.sensors[msg.node_id].sketch_version = msg.payload self.alert(msg.node_id) elif msg.sub_type == self.const.Internal.I_CONFIG: return msg.copy(ack=0, payload='M' if self.metric else 'I') elif msg.sub_type == self.const.Internal.I_BATTERY_LEVEL: if self.is_sensor(msg.node_id): self.sensors[msg.node_id].battery_level = int(msg.payload) self.alert(msg.node_id) elif msg.sub_type == self.const.Internal.I_TIME: return msg.copy(ack=0, payload=int(time.time())) elif (self.protocol_version >= 2.0 and msg.sub_type == self.const.Internal.I_HEARTBEAT_RESPONSE): self._handle_heartbeat(msg) elif msg.sub_type == self.const.Internal.I_LOG_MESSAGE and self.debug: _LOGGER.debug('n:%s c:%s t:%s s:%s p:%s', msg.node_id, msg.child_id, msg.type, msg.sub_type, msg.payload) def _handle_stream(self, msg): """Process a stream type message.""" if not self.is_sensor(msg.node_id): return if msg.sub_type == self.const.Stream.ST_FIRMWARE_CONFIG_REQUEST: return self.ota.respond_fw_config(msg) elif msg.sub_type == self.const.Stream.ST_FIRMWARE_REQUEST: return self.ota.respond_fw(msg) def send(self, message): """Should be implemented by a child class.""" raise NotImplementedError def logic(self, data): """Parse the data and respond to it appropriately. Response is returned to the caller and has to be sent data as a mysensors command string. """ ret = None try: msg = Message(data) except ValueError: return if msg.type == self.const.MessageType.presentation: ret = self._handle_presentation(msg) elif msg.type == self.const.MessageType.set: ret = self._handle_set(msg) elif msg.type == self.const.MessageType.req: ret = self._handle_req(msg) elif msg.type == self.const.MessageType.internal: ret = self._handle_internal(msg) elif msg.type == self.const.MessageType.stream: ret = self._handle_stream(msg) ret = self._route_message(ret) ret = ret.encode() if ret else None return ret def _save_pickle(self, filename): """Save sensors to pickle file.""" with open(filename, 'wb') as file_handle: pickle.dump(self.sensors, file_handle, pickle.HIGHEST_PROTOCOL) file_handle.flush() os.fsync(file_handle.fileno()) def _load_pickle(self, filename): """Load sensors from pickle file.""" with open(filename, 'rb') as file_handle: self.sensors = pickle.load(file_handle) def _save_json(self, filename): """Save sensors to json file.""" with open(filename, 'w') as file_handle: json.dump(self.sensors, file_handle, cls=MySensorsJSONEncoder) file_handle.flush() os.fsync(file_handle.fileno()) def _load_json(self, filename): """Load sensors from json file.""" with open(filename, 'r') as file_handle: self.sensors = json.load(file_handle, cls=MySensorsJSONDecoder) def _save_sensors(self): """Save sensors to file.""" fname = os.path.realpath(self.persistence_file) exists = os.path.isfile(fname) dirname = os.path.dirname(fname) if exists and os.access(fname, os.W_OK) and \ os.access(dirname, os.W_OK) or \ not exists and os.access(dirname, os.W_OK): split_fname = os.path.splitext(fname) tmp_fname = '{}.tmp{}'.format(split_fname[0], split_fname[1]) self._perform_file_action(tmp_fname, 'save') if exists: os.rename(fname, self.persistence_bak) os.rename(tmp_fname, fname) if exists: os.remove(self.persistence_bak) else: _LOGGER.error('Permission denied when writing to %s', fname) def _load_sensors(self, path=None): """Load sensors from file.""" if path is None: path = self.persistence_file exists = os.path.isfile(path) if exists and os.access(path, os.R_OK): if path == self.persistence_bak: os.rename(path, self.persistence_file) path = self.persistence_file self._perform_file_action(path, 'load') return True else: _LOGGER.warning('File does not exist or is not readable: %s', path) return False def _safe_load_sensors(self): """Load sensors safely from file.""" try: loaded = self._load_sensors() except (EOFError, ValueError): _LOGGER.error('Bad file contents: %s', self.persistence_file) loaded = False if not loaded: _LOGGER.warning('Trying backup file: %s', self.persistence_bak) try: if not self._load_sensors(self.persistence_bak): _LOGGER.warning('Failed to load sensors from file: %s', self.persistence_file) except (EOFError, ValueError): _LOGGER.error('Bad file contents: %s', self.persistence_file) _LOGGER.warning('Removing file: %s', self.persistence_file) os.remove(self.persistence_file) def _perform_file_action(self, filename, action): """Perform action on specific file types. Dynamic dispatch function for performing actions on specific file types. """ ext = os.path.splitext(filename)[1] func = getattr(self, '_%s_%s' % (action, ext[1:]), None) if func is None: raise Exception('Unsupported file type %s' % ext[1:]) func(filename) def alert(self, nid): """Tell anyone who wants to know that a sensor was updated. Also save sensors if persistence is enabled. """ if self.event_callback is not None: try: self.event_callback('sensor_update', nid) except Exception as exception: # pylint: disable=W0703 _LOGGER.exception(exception) if self.persistence: self._save_sensors() def _get_next_id(self): """Return the next available sensor id.""" if self.sensors: next_id = max(self.sensors.keys()) + 1 else: next_id = 1 if next_id <= 254: return next_id def add_sensor(self, sensorid=None): """Add a sensor to the gateway.""" if sensorid is None: sensorid = self._get_next_id() if sensorid is not None and sensorid not in self.sensors: self.sensors[sensorid] = Sensor(sensorid) return sensorid def is_sensor(self, sensorid, child_id=None): """Return True if a sensor and its child exist.""" ret = sensorid in self.sensors if not ret: _LOGGER.warning('Node %s is unknown', sensorid) if ret and child_id is not None: ret = child_id in self.sensors[sensorid].children if not ret: _LOGGER.warning('Child %s is unknown', child_id) if not ret and self.protocol_version >= 2.0: _LOGGER.info('Requesting new presentation for node %s', sensorid) msg = Message().copy(node_id=sensorid, child_id=255, type=self.const.MessageType.internal, sub_type=self.const.Internal.I_PRESENTATION) if self._route_message(msg): self.fill_queue(msg.encode) return ret def _route_message(self, msg): if not isinstance(msg, Message): return if (msg.node_id not in self.sensors or msg.type == self.const.MessageType.stream or not self.sensors[msg.node_id].new_state): return msg self.sensors[msg.node_id].queue.append(msg.encode()) def handle_queue(self, queue=None): """Handle queue. If queue is not empty, get the function and any args and kwargs from the queue. Run the function and return output. """ if queue is None: queue = self.queue if not queue.empty(): func, args, kwargs = queue.get() reply = func(*args, **kwargs) queue.task_done() return reply def fill_queue(self, func, args=None, kwargs=None, queue=None): """Put a function in a queue. Put the function 'func', a tuple of arguments 'args' and a dict of keyword arguments 'kwargs', as a tuple in the queue. """ if args is None: args = () if kwargs is None: kwargs = {} if queue is None: queue = self.queue queue.put((func, args, kwargs)) def set_child_value(self, sensor_id, child_id, value_type, value, **kwargs): """Add a command to set a sensor value, to the queue. A queued command will be sent to the sensor when the gateway thread has sent all previously queued commands to the FIFO queue. If the sensor attribute new_state returns True, the command will not be put on the queue, but the internal sensor state will be updated. When a heartbeat response is received, the internal state will be pushed to the sensor, via _handle_heartbeat method. """ if not self.is_sensor(sensor_id, child_id): return if self.sensors[sensor_id].new_state: self.sensors[sensor_id].set_child_value( child_id, value_type, value, children=self.sensors[sensor_id].new_state) else: self.fill_queue(self.sensors[sensor_id].set_child_value, (child_id, value_type, value), kwargs) def update_fw(self, nids, fw_type, fw_ver, fw_path=None): """Update firwmare of all node_ids in nids.""" self.ota.make_update(nids, fw_type, fw_ver, fw_path)
class Gateway(object): """Base implementation for a MySensors Gateway.""" # pylint: disable=too-many-instance-attributes def __init__(self, event_callback=None, persistence=False, persistence_file='mysensors.pickle', protocol_version='1.4'): """Setup Gateway.""" self.queue = Queue() self.lock = threading.Lock() self.event_callback = event_callback self.sensors = {} self.metric = True # if true - use metric, if false - use imperial self.debug = False # if true - print all received messages self.persistence = persistence # if true - save sensors to disk self.persistence_file = persistence_file # path to persistence file self.persistence_bak = '{}.bak'.format(self.persistence_file) self.protocol_version = float(protocol_version) if 1.5 <= self.protocol_version < 2.0: _const = import_module('mysensors.const_15') elif self.protocol_version >= 2.0: _const = import_module('mysensors.const_20') else: _const = import_module('mysensors.const_14') self.const = _const self.ota = OTAFirmware(self.sensors, self.const) if persistence: self._safe_load_sensors() def _handle_presentation(self, msg): """Process a presentation message.""" if msg.child_id == 255: # this is a presentation of the sensor platform sensorid = self.add_sensor(msg.node_id) if sensorid is None: return self.sensors[msg.node_id].type = msg.sub_type self.sensors[msg.node_id].protocol_version = msg.payload self.sensors[msg.node_id].reboot = False self.alert(msg.node_id) return msg else: # this is a presentation of a child sensor if not self.is_sensor(msg.node_id): _LOGGER.error('Node %s is unknown, will not add child %s.', msg.node_id, msg.child_id) return child_id = self.sensors[msg.node_id].add_child_sensor( msg.child_id, msg.sub_type, msg.payload) if child_id is None: return self.alert(msg.node_id) return msg def _handle_set(self, msg): """Process a set message.""" if not self.is_sensor(msg.node_id, msg.child_id): return self.sensors[msg.node_id].set_child_value( msg.child_id, msg.sub_type, msg.payload) if self.sensors[msg.node_id].new_state: self.sensors[msg.node_id].set_child_value( msg.child_id, msg.sub_type, msg.payload, children=self.sensors[msg.node_id].new_state) self.alert(msg.node_id) # Check if reboot is true if self.sensors[msg.node_id].reboot: return msg.copy( child_id=255, type=self.const.MessageType.internal, ack=0, sub_type=self.const.Internal.I_REBOOT, payload='') def _handle_req(self, msg): """Process a req message. This will return the value if it exists. If no value exists, nothing is returned. """ if self.is_sensor(msg.node_id, msg.child_id): value = self.sensors[msg.node_id].children[ msg.child_id].values.get(msg.sub_type) if value is not None: return msg.copy(type=self.const.MessageType.set, payload=value) def _handle_heartbeat(self, msg): """Process a heartbeat message.""" if not self.is_sensor(msg.node_id): return while self.sensors[msg.node_id].queue: self.fill_queue(str, (self.sensors[msg.node_id].queue.popleft(), )) for child in self.sensors[msg.node_id].children.values(): new_child = self.sensors[msg.node_id].new_state.get( child.id, ChildSensor(child.id, child.type, child.description)) self.sensors[msg.node_id].new_state[child.id] = new_child for value_type, value in child.values.items(): new_value = new_child.values.get(value_type) if new_value is not None and new_value != value: self.fill_queue(self.sensors[msg.node_id].set_child_value, (child.id, value_type, new_value)) def _handle_internal(self, msg): """Process an internal protocol message.""" if msg.sub_type == self.const.Internal.I_ID_REQUEST: node_id = self.add_sensor() return msg.copy(ack=0, sub_type=self.const.Internal.I_ID_RESPONSE, payload=node_id) if node_id is not None else None elif msg.sub_type == self.const.Internal.I_SKETCH_NAME: if self.is_sensor(msg.node_id): self.sensors[msg.node_id].sketch_name = msg.payload self.alert(msg.node_id) elif msg.sub_type == self.const.Internal.I_SKETCH_VERSION: if self.is_sensor(msg.node_id): self.sensors[msg.node_id].sketch_version = msg.payload self.alert(msg.node_id) elif msg.sub_type == self.const.Internal.I_CONFIG: return msg.copy(ack=0, payload='M' if self.metric else 'I') elif msg.sub_type == self.const.Internal.I_BATTERY_LEVEL: if self.is_sensor(msg.node_id): self.sensors[msg.node_id].battery_level = int(msg.payload) self.alert(msg.node_id) elif msg.sub_type == self.const.Internal.I_TIME: return msg.copy(ack=0, payload=int(time.time())) elif (self.protocol_version >= 2.0 and msg.sub_type == self.const.Internal.I_HEARTBEAT_RESPONSE): self._handle_heartbeat(msg) elif msg.sub_type == self.const.Internal.I_LOG_MESSAGE and self.debug: _LOGGER.debug('n:%s c:%s t:%s s:%s p:%s', msg.node_id, msg.child_id, msg.type, msg.sub_type, msg.payload) def _handle_stream(self, msg): """Process a stream type message.""" if not self.is_sensor(msg.node_id): return if msg.sub_type == self.const.Stream.ST_FIRMWARE_CONFIG_REQUEST: return self.ota.respond_fw_config(msg) elif msg.sub_type == self.const.Stream.ST_FIRMWARE_REQUEST: return self.ota.respond_fw(msg) def send(self, message): """Should be implemented by a child class.""" raise NotImplementedError def logic(self, data): """Parse the data and respond to it appropriately. Response is returned to the caller and has to be sent data as a mysensors command string. """ ret = None try: msg = Message(data) except ValueError: return if msg.type == self.const.MessageType.presentation: ret = self._handle_presentation(msg) elif msg.type == self.const.MessageType.set: ret = self._handle_set(msg) elif msg.type == self.const.MessageType.req: ret = self._handle_req(msg) elif msg.type == self.const.MessageType.internal: ret = self._handle_internal(msg) elif msg.type == self.const.MessageType.stream: ret = self._handle_stream(msg) ret = self._route_message(ret) ret = ret.encode() if ret else None return ret def _save_pickle(self, filename): """Save sensors to pickle file.""" with open(filename, 'wb') as file_handle: pickle.dump(self.sensors, file_handle, pickle.HIGHEST_PROTOCOL) file_handle.flush() os.fsync(file_handle.fileno()) def _load_pickle(self, filename): """Load sensors from pickle file.""" with open(filename, 'rb') as file_handle: self.sensors = pickle.load(file_handle) def _save_json(self, filename): """Save sensors to json file.""" with open(filename, 'w') as file_handle: json.dump(self.sensors, file_handle, cls=MySensorsJSONEncoder) file_handle.flush() os.fsync(file_handle.fileno()) def _load_json(self, filename): """Load sensors from json file.""" with open(filename, 'r') as file_handle: self.sensors = json.load(file_handle, cls=MySensorsJSONDecoder) def _save_sensors(self): """Save sensors to file.""" fname = os.path.realpath(self.persistence_file) exists = os.path.isfile(fname) dirname = os.path.dirname(fname) if exists and os.access(fname, os.W_OK) and \ os.access(dirname, os.W_OK) or \ not exists and os.access(dirname, os.W_OK): split_fname = os.path.splitext(fname) tmp_fname = '{}.tmp{}'.format(split_fname[0], split_fname[1]) self._perform_file_action(tmp_fname, 'save') if exists: os.rename(fname, self.persistence_bak) os.rename(tmp_fname, fname) if exists: os.remove(self.persistence_bak) else: _LOGGER.error('Permission denied when writing to %s', fname) def _load_sensors(self, path=None): """Load sensors from file.""" if path is None: path = self.persistence_file exists = os.path.isfile(path) if exists and os.access(path, os.R_OK): if path == self.persistence_bak: os.rename(path, self.persistence_file) path = self.persistence_file self._perform_file_action(path, 'load') return True else: _LOGGER.warning('File does not exist or is not readable: %s', path) return False def _safe_load_sensors(self): """Load sensors safely from file.""" try: loaded = self._load_sensors() except (EOFError, ValueError): _LOGGER.error('Bad file contents: %s', self.persistence_file) loaded = False if not loaded: _LOGGER.warning('Trying backup file: %s', self.persistence_bak) try: if not self._load_sensors(self.persistence_bak): _LOGGER.warning('Failed to load sensors from file: %s', self.persistence_file) except (EOFError, ValueError): _LOGGER.error('Bad file contents: %s', self.persistence_file) _LOGGER.warning('Removing file: %s', self.persistence_file) os.remove(self.persistence_file) def _perform_file_action(self, filename, action): """Perform action on specific file types. Dynamic dispatch function for performing actions on specific file types. """ ext = os.path.splitext(filename)[1] func = getattr(self, '_%s_%s' % (action, ext[1:]), None) if func is None: raise Exception('Unsupported file type %s' % ext[1:]) func(filename) def alert(self, nid): """Tell anyone who wants to know that a sensor was updated. Also save sensors if persistence is enabled. """ if self.event_callback is not None: try: self.event_callback('sensor_update', nid) except Exception as exception: # pylint: disable=W0703 _LOGGER.exception(exception) if self.persistence: self._save_sensors() def _get_next_id(self): """Return the next available sensor id.""" if self.sensors: next_id = max(self.sensors.keys()) + 1 else: next_id = 1 if next_id <= 254: return next_id def add_sensor(self, sensorid=None): """Add a sensor to the gateway.""" if sensorid is None: sensorid = self._get_next_id() if sensorid is not None and sensorid not in self.sensors: self.sensors[sensorid] = Sensor(sensorid) return sensorid def is_sensor(self, sensorid, child_id=None): """Return True if a sensor and its child exist.""" ret = sensorid in self.sensors if not ret: _LOGGER.warning('Node %s is unknown', sensorid) if ret and child_id is not None: ret = child_id in self.sensors[sensorid].children if not ret: _LOGGER.warning('Child %s is unknown', child_id) if not ret and self.protocol_version >= 2.0: _LOGGER.info('Requesting new presentation for node %s', sensorid) msg = Message().copy( node_id=sensorid, child_id=255, type=self.const.MessageType.internal, sub_type=self.const.Internal.I_PRESENTATION) if self._route_message(msg): self.fill_queue(msg.encode) return ret def _route_message(self, msg): if not isinstance(msg, Message) or \ msg.type == self.const.MessageType.presentation: return if (msg.node_id not in self.sensors or msg.type == self.const.MessageType.stream or not self.sensors[msg.node_id].new_state): return msg self.sensors[msg.node_id].queue.append(msg.encode()) def handle_queue(self, queue=None): """Handle queue. If queue is not empty, get the function and any args and kwargs from the queue. Run the function and return output. """ if queue is None: queue = self.queue if not queue.empty(): func, args, kwargs = queue.get() reply = func(*args, **kwargs) queue.task_done() return reply def fill_queue(self, func, args=None, kwargs=None, queue=None): """Put a function in a queue. Put the function 'func', a tuple of arguments 'args' and a dict of keyword arguments 'kwargs', as a tuple in the queue. """ if args is None: args = () if kwargs is None: kwargs = {} if queue is None: queue = self.queue queue.put((func, args, kwargs)) def set_child_value( self, sensor_id, child_id, value_type, value, **kwargs): """Add a command to set a sensor value, to the queue. A queued command will be sent to the sensor when the gateway thread has sent all previously queued commands to the FIFO queue. If the sensor attribute new_state returns True, the command will not be put on the queue, but the internal sensor state will be updated. When a heartbeat response is received, the internal state will be pushed to the sensor, via _handle_heartbeat method. """ if not self.is_sensor(sensor_id, child_id): return if self.sensors[sensor_id].new_state: self.sensors[sensor_id].set_child_value( child_id, value_type, value, children=self.sensors[sensor_id].new_state) else: self.fill_queue(self.sensors[sensor_id].set_child_value, (child_id, value_type, value), kwargs) def update_fw(self, nids, fw_type, fw_ver, fw_path=None): """Update firwmare of all node_ids in nids.""" self.ota.make_update(nids, fw_type, fw_ver, fw_path)