async def test_write(app, init_app, client, m_writer): service_status.set_autoconnecting(app, True) await asyncio.sleep(0.01) conn = connection.fget(app) assert conn.connected assert service_status.desc(app).is_connected assert service_status.desc(app).connection_kind == 'usb' await conn.write('testey') m_writer.write.assert_called_once_with(b'testey\n') m_writer.drain.assert_awaited_once() m_writer.reset_mock() await conn.write(b'testey') m_writer.write.assert_called_once_with(b'testey\n') m_writer.drain.assert_awaited_once() await conn.start_reconnect() m_writer.close.assert_called_once() m_writer.is_closing.return_value = True assert not conn.connected await conn.start_reconnect() m_writer.close.assert_called_once() with pytest.raises(exceptions.NotConnected): await conn.write('stuff')
async def flash(self) -> dict: # pragma: no cover sender = ymodem.FileSender(self._notify) cmder = commander.fget(self.app) address = service_status.desc(self.app).device_address self._notify( f'Started updating {self.name}@{address} to version {self.version} ({self.date})' ) try: if not service_status.desc(self.app).is_connected: self._notify('Controller is not connected. Aborting update.') raise exceptions.NotConnected() if self.simulation: raise NotImplementedError( 'Firmware updates not available for simulation controllers' ) self._notify('Sending update command to controller') service_status.set_updating(self.app) await asyncio.sleep(FLUSH_PERIOD_S ) # Wait for in-progress commands to finish await cmder.execute(commands.FirmwareUpdateCommand.from_args()) self._notify('Shutting down normal communication') await cmder.shutdown(self.app) self._notify('Waiting for normal connection to close') await asyncio.wait_for(service_status.wait_disconnected(self.app), STATE_TIMEOUT_S) self._notify(f'Connecting to {address}') conn = await self._connect(address) with conn.autoclose(): await asyncio.wait_for(sender.transfer(conn), TRANSFER_TIMEOUT_S) except Exception as ex: self._notify(f'Failed to update firmware: {strex(ex)}') raise exceptions.FirmwareUpdateFailed(strex(ex)) finally: self._notify('Scheduling service reboot') await shutdown_soon(self.app, UPDATE_SHUTDOWN_DELAY_S) self._notify('Firmware updated!') return {'address': address, 'version': self.version}
async def set_display_settings(self): write_required = False if not service_status.desc(self.app).is_acknowledged: return display_block = await self.commander.read_block( FirmwareBlockIdentity(nid=const.DISPLAY_SETTINGS_NID)) user_unit = self.global_store.units['temperature'] expected_unit = 'TEMP_FAHRENHEIT' if user_unit == 'degF' else 'TEMP_CELSIUS' block_unit = display_block.data['tempUnit'] if expected_unit != block_unit: write_required = True display_block.data['tempUnit'] = expected_unit LOGGER.info(f'Spark display temperature unit set to {user_unit}') user_tz_name = self.global_store.time_zone['name'] user_tz = self.global_store.time_zone['posixValue'] block_tz = display_block.data['timeZone'] if user_tz != block_tz: write_required = True display_block.data['timeZone'] = user_tz LOGGER.info( f'Spark display time zone set to {user_tz} ({user_tz_name})') if write_required: await self.commander.write_block(display_block)
async def test_callback(app, init_app, client, m_reader, m_writer): service_status.set_autoconnecting(app, True) await asyncio.sleep(0.01) conn = connection.fget(app) m_data_cb = Mock() m_data_cb2 = Mock() conn.data_callbacks.add(m_data_cb) conn.data_callbacks.add(m_data_cb2) m_reader.feed_data('<!connected:sensor>bunnies<fluffy>\n'.encode()) await asyncio.sleep(0.01) m_data_cb.assert_called_with('bunnies') m_data_cb2.assert_called_with('bunnies') # Close it down m_reader.feed_data('puppies\n'.encode()) m_writer.is_closing.return_value = True await asyncio.sleep(0.01) m_data_cb.assert_called_with('puppies') m_data_cb2.assert_called_with('puppies') assert conn.active assert not conn.connected assert not service_status.desc(app).is_connected
async def set_display_settings(self): if not service_status.desc(self.app).is_acknowledged: return store = global_store.fget(self.app) controller = spark.fget(self.app) write_required = False display_block = await controller.read_object({ 'nid': const.DISPLAY_SETTINGS_NID }) user_unit = store.units['temperature'] expected_unit = 'TEMP_FAHRENHEIT' if user_unit == 'degF' else 'TEMP_CELSIUS' block_unit = display_block['data']['tempUnit'] if expected_unit != block_unit: write_required = True display_block['data']['tempUnit'] = expected_unit LOGGER.info(f'Spark display temperature unit set to {user_unit}') user_tz_name = store.time_zone['name'] user_tz = store.time_zone['posixValue'] block_tz = display_block['data']['timeZone'] if user_tz != block_tz: write_required = True display_block['data']['timeZone'] = user_tz LOGGER.info(f'Spark display time zone set to {user_tz} ({user_tz_name})') if write_required: await controller.write_object(display_block)
async def get(self) -> r200[StatusDescription]: """ Get service status Tags: System """ desc = service_status.desc(self.request.app) return web.json_response(desc.dict())
async def run(self): try: await asyncio.sleep(self.interval) synched = await service_status.wait_synchronized(self.app, wait=False) status_data = service_status.desc(self.app) blocks = [] try: if synched: blocks, logged_blocks = await controller.fget(self.app).read_all_broadcast_blocks() # Convert list to key/value format suitable for history history_data = { block.id: block.data for block in logged_blocks if not block.id.startswith(const.GENERATED_ID_PREFIX) } await mqtt.publish(self.app, self.history_topic, err=False, message={ 'key': self.name, 'data': history_data, }) finally: # State event is always published await mqtt.publish(self.app, self.state_topic, err=False, retain=True, message={ 'key': self.name, 'type': 'Spark.state', 'data': { 'status': status_data.dict(), 'blocks': [v.dict() for v in blocks], 'relations': calculate_relations(blocks), 'drive_chains': calculate_drive_chains(blocks), }, }) except Exception as ex: LOGGER.debug(f'{self} exception: {strex(ex)}') raise ex
async def _sync_handshake(self): """ Wait for the controller to acknowledge the connection with a handshake, while sending prompts by using the Noop command. If no handshake is received after `HANDSHAKE_TIMEOUT_S`, an asyncio.TimeoutError is raised. The handshake is checked, and appropriate errors are raised if the device ID or firmware version are incompatible. """ async def prompt_handshake(): while True: try: await asyncio.sleep(PING_INTERVAL_S) LOGGER.info('prompting handshake...') await self.commander.version() except Exception as ex: LOGGER.error(strex(ex)) pass ack_wait_task = asyncio.create_task( service_status.wait_acknowledged(self.app)) prompt_task = asyncio.create_task(prompt_handshake()) await asyncio.wait([ack_wait_task, prompt_task], return_when=asyncio.FIRST_COMPLETED, timeout=HANDSHAKE_TIMEOUT_S) # asyncio.wait() does not cancel tasks # cancel() can be safely called if the task is already done ack_wait_task.cancel() prompt_task.cancel() if not await service_status.wait_acknowledged(self.app, wait=False): raise asyncio.TimeoutError() handshake = service_status.desc(self.app).handshake_info if not handshake.is_compatible_firmware: raise exceptions.IncompatibleFirmware() if not handshake.is_valid_device_id: raise exceptions.InvalidDeviceId()
async def flash(self) -> dict: # pragma: no cover ota = ymodem.OtaClient(self._notify) cmder = commander.fget(self.app) status_desc = service_status.desc(self.app) address = status_desc.device_address platform = status_desc.device_info.platform self._notify( f'Started updating {self.name}@{address} to version {self.version} ({self.date})' ) try: if not status_desc.is_connected: self._notify('Controller is not connected. Aborting update.') raise exceptions.NotConnected() if self.simulation: raise NotImplementedError( 'Firmware updates not available for simulation controllers' ) self._notify('Preparing update') service_status.set_updating(self.app) await asyncio.sleep(FLUSH_PERIOD_S ) # Wait for in-progress commands to finish if platform != 'esp32': # pragma: no cover self._notify('Sending update command to controller') await cmder.execute(commands.FirmwareUpdateCommand.from_args()) self._notify('Waiting for normal connection to close') await cmder.shutdown(self.app) await asyncio.wait_for(service_status.wait_disconnected(self.app), STATE_TIMEOUT_S) if platform == 'esp32': # pragma: no cover # ESP connections will always be a TCP address host, _ = address.split(':') self._notify(f'Sending update prompt to {host}') self._notify( 'The Spark will now download and apply the new firmware') self._notify('The update is done when the service reconnects') fw_url = ESP_URL_FMT.format(**self.app['ini']) await http.session(self.app ).post(f'http://{host}:80/firmware_update', data=fw_url) else: self._notify(f'Connecting to {address}') conn = await ymodem.connect(address) with conn.autoclose(): await asyncio.wait_for( ota.send(conn, f'firmware/brewblox-{platform}.bin'), TRANSFER_TIMEOUT_S) self._notify('Update done!') except Exception as ex: self._notify(f'Failed to update firmware: {strex(ex)}') raise exceptions.FirmwareUpdateFailed(strex(ex)) finally: self._notify('Restarting service...') await shutdown_soon(self.app, UPDATE_SHUTDOWN_DELAY_S) return {'address': address, 'version': self.version}
def get_autoconnecting(self): return service_status.desc(self.app).is_autoconnecting
def device_name(self) -> str: # Simulation services are identified by service name if self.app['config']['simulation']: return 'simulator__' + self.app['config']['name'] return service_status.desc(self.app).device_info.device_id
async def test_sim_status(app, init_app, client, m_connect): app['config']['simulation'] = True service_status.set_autoconnecting(app, True) await asyncio.sleep(0.01) assert service_status.desc(app).connection_kind == 'simulation'