def test_one_minute_schedule(self): now_offset = 1577836800 # 2020 minute = 60 fakesleep.reset(seconds=now_offset) SchedulingController.TIMEZONE = 'Europe/Brussels' schedule = ScheduleDTO(id=1, name='schedule', start=0 * minute, repeat='* * * * *', duration=None, end=now_offset + 60 * minute, action='GROUP_ACTION', arguments=1, status='ACTIVE') schedule.next_execution = SchedulingController._get_next_execution( schedule) self.assertFalse(schedule.is_due) schedule.next_execution = SchedulingController._get_next_execution( schedule) self.assertEqual(now_offset + 1 * minute, schedule.next_execution) time.sleep(1 * minute) self.assertTrue(schedule.is_due) schedule.next_execution = SchedulingController._get_next_execution( schedule) self.assertEqual(now_offset + 2 * minute, schedule.next_execution) time.sleep(1 * minute) self.assertTrue(schedule.is_due) schedule.next_execution = SchedulingController._get_next_execution( schedule) self.assertEqual(now_offset + 3 * minute, schedule.next_execution)
def test_shutter_sync_state(self): master_controller = Mock() master_controller.load_shutters = lambda: [] SetUpTestInjections(master_controller=master_controller, maintenance_controller=Mock()) controller = ShutterController() # Basic configuration controller.update_config(ShutterControllerTest.SHUTTER_CONFIG) self.assertEqual(len(controller._shutters), 4) events = [] def on_change(gateway_event): events.append(gateway_event) self.pubsub.subscribe_gateway_events(PubSub.GatewayTopics.STATE, on_change) controller.start() self.pubsub._publish_all_events() self.assertEqual([GatewayEvent('SHUTTER_CHANGE', {'id': 0, 'status': {'state': 'STOPPED', 'position': None, 'last_change': 0.0}, 'location': {'room_id': None}}), GatewayEvent('SHUTTER_CHANGE', {'id': 1, 'status': {'state': 'STOPPED', 'position': None, 'last_change': 0.0}, 'location': {'room_id': None}}), GatewayEvent('SHUTTER_CHANGE', {'id': 2, 'status': {'state': 'STOPPED', 'position': None, 'last_change': 0.0}, 'location': {'room_id': None}}), GatewayEvent('SHUTTER_CHANGE', {'id': 3, 'status': {'state': 'STOPPED', 'position': None, 'last_change': 0.0}, 'location': {'room_id': None}})], events) events = [] fakesleep.reset(100) controller.report_shutter_position(0, 89, 'UP') self.pubsub._publish_all_events() self.assertEqual([GatewayEvent('SHUTTER_CHANGE', {'id': 0, 'status': {'state': 'GOING_UP', 'position': 89, 'last_change': 100.0}, 'location': {'room_id': None}})], events) controller.stop()
def test_midnight_crossover(self): now_offset = 1615939080 # 16/03/21 23:58 minute = 60 fakesleep.reset(seconds=now_offset ) # Tricks the system to be in the time we specified SchedulingController.TIMEZONE = 'Europe/Brussels' schedule = ScheduleDTO(id=1, name='schedule', start=0 * minute, repeat='* * * * *', duration=None, end=now_offset + 10 * minute, action='GROUP_ACTION', arguments=1, status='ACTIVE') schedule.next_execution = SchedulingController._get_next_execution( schedule) self.assertFalse(schedule.is_due) schedule.next_execution = SchedulingController._get_next_execution( schedule) self.assertEqual(now_offset + 1 * minute, schedule.next_execution) time.sleep(1 * minute) self.assertTrue(schedule.is_due) schedule.next_execution = SchedulingController._get_next_execution( schedule) self.assertEqual(now_offset + 2 * minute, schedule.next_execution) time.sleep(1 * minute) self.assertTrue(schedule.is_due) schedule.next_execution = SchedulingController._get_next_execution( schedule) self.assertEqual(now_offset + 3 * minute, schedule.next_execution)
def set(self, seconds=None): """Set the global fake time to the given epoch or the real current one. :param seconds: Fake current time, in seconds since the epoch. If ``None``, use the real current time. """ reset(seconds=seconds)
def test_schedule_is_due(self): now_offset = 1577836800 # 2020-01-01 offset_2018 = 1514764800 # 2018-01-01 minute = 60 hour = 60 * minute fakesleep.reset(seconds=offset_2018) SchedulingController.TIMEZONE = 'Europe/Brussels' schedule = ScheduleDTO(id=1, name='schedule', start=offset_2018, repeat='0 * * * *', duration=None, end=now_offset + 24 * hour, action='GROUP_ACTION', arguments=1, status='ACTIVE') schedule.next_execution = SchedulingController._get_next_execution( schedule) self.assertFalse(schedule.is_due) schedule.next_execution = SchedulingController._get_next_execution( schedule) self.assertEqual(SchedulingController.NO_NTP_LOWER_LIMIT + 1 * hour, schedule.next_execution) time.sleep(1 * hour) self.assertFalse(schedule.is_due) # Date is before 2019 schedule.next_execution = SchedulingController._get_next_execution( schedule) self.assertEqual(SchedulingController.NO_NTP_LOWER_LIMIT + 1 * hour, schedule.next_execution) fakesleep.reset(seconds=now_offset) self.assertFalse(schedule.is_due) # Time jump is ignored schedule.next_execution = SchedulingController._get_next_execution( schedule) self.assertEqual(now_offset + 1 * hour, schedule.next_execution) time.sleep(1 * hour) self.assertTrue(schedule.is_due) schedule.next_execution = SchedulingController._get_next_execution( schedule) self.assertEqual(now_offset + 2 * hour, schedule.next_execution)
def test_edge_cases_cron(self): now_offset = 1616371020 # Date and time: Sunday, March 21, 2021 11:57:00 PM GMT minute = 60 SchedulingController.TIMEZONE = 'Europe/Brussels' fakesleep.reset(seconds=now_offset) SchedulingController.TIMEZONE = 'Europe/Brussels' schedule = ScheduleDTO( id=1, name='schedule', start=200, # Thursday, January 1, 1970 1:03:20 AM repeat='* * * * *', # every minute duration=None, end=now_offset + 10 * minute, # for 10 minuted action='GROUP_ACTION', arguments=1, status='ACTIVE') # Should be 1616371020 + 60 = 1616371080 schedule.next_execution = SchedulingController._get_next_execution( schedule) self.assertFalse(schedule.is_due) # Should still be 1616371020 + 60 = 1616371080 for i in range(1, 11): schedule.next_execution = SchedulingController._get_next_execution( schedule) self.assertEqual(now_offset + i * minute, schedule.next_execution) time.sleep(minute) self.assertTrue(schedule.is_due) # now testing end of month behaviour now_offset = 1617235020 # 2021-03-31 23:57:00 fakesleep.reset(seconds=now_offset) schedule.end = now_offset + 10 * minute # Update when the schedule should end for i in range(1, 11): schedule.next_execution = SchedulingController._get_next_execution( schedule) self.assertEqual(now_offset + i * minute, schedule.next_execution) time.sleep(minute) self.assertTrue(schedule.is_due)
def test_events_and_state(self): fakesleep.reset(0) calls = {} SetUpTestInjections(master_communicator=Mock(), configuration_controller=Mock(), eeprom_controller=Mock()) master_controller = MasterClassicController() master_controller._master_version = (3, 143, 103) master_controller._shutter_config = {shutter.id: shutter for shutter in ShutterControllerTest.SHUTTER_CONFIG} SetUpTestInjections(master_controller=master_controller, maintenance_controller=Mock()) controller = ShutterController() controller.update_config(ShutterControllerTest.SHUTTER_CONFIG) def shutter_callback(event): calls.setdefault(event.data['id'], []).append(event.data['status']['state']) self.pubsub.subscribe_gateway_events(PubSub.GatewayTopics.STATE, shutter_callback) self.pubsub._publish_all_events() def validate(_shutter_id, _entry): self.pubsub._publish_all_events() self.assertEqual(controller._actual_positions.get(_shutter_id), _entry[0]) self.assertEqual(controller._desired_positions.get(_shutter_id), _entry[1]) self.assertEqual(controller._directions.get(_shutter_id), _entry[2]) timer, state = controller._states.get(_shutter_id) self.assertEqual(timer, _entry[3][0]) self.assertEqual(state, _entry[3][1]) if len(_entry) == 4 or _entry[4]: self.assertEqual(calls[_shutter_id].pop(), _entry[3][1].upper()) master_controller._update_from_master_state({'module_nr': 0, 'status': 0b00000000}) self.pubsub._publish_all_events() for shutter_id in range(3): # +- actual position # | +- desired position # | | +- direction +- state +- optional skip call check # v v v v v validate(shutter_id, [None, None, ShutterEnums.Direction.STOP, (0.0, ShutterEnums.State.STOPPED), False]) ################################################################################################### # set stutters to a known initial state for shutter in self.SHUTTER_CONFIG: controller._directions[shutter.id] = ShutterEnums.Direction.UP controller._actual_positions[shutter.id] = 0 ################################################################################################### for shutter_id in range(3): controller.shutter_down(shutter_id, None) self.pubsub._publish_all_events() time.sleep(20) master_controller._update_from_master_state({'module_nr': 0, 'status': 0b00011001}) self.pubsub._publish_all_events() # +- actual position # | +- desired position # | | +- direction +- state # v v v v for shutter_id, entry in {0: [0, 99, ShutterEnums.Direction.DOWN, (20, ShutterEnums.State.GOING_DOWN)], 1: [0, 99, ShutterEnums.Direction.DOWN, (20, ShutterEnums.State.GOING_DOWN)], # this shutter is inverted 2: [0, 79, ShutterEnums.Direction.DOWN, (20, ShutterEnums.State.GOING_DOWN)]}.items(): validate(shutter_id, entry) self.pubsub._publish_all_events() time.sleep(50) # Standard shutters will still be going down controller._actual_positions[2] = 20 # Simulate position reporting master_controller._update_from_master_state({'module_nr': 0, 'status': 0b00011000}) # First shutter motor stop self.pubsub._publish_all_events() # +- actual position # | +- desired position # | | +- direction +- state +- optional skip call check # v v v v v for shutter_id, entry in {0: [25, 99, ShutterEnums.Direction.STOP, (70, ShutterEnums.State.STOPPED)], 1: [0, 99, ShutterEnums.Direction.DOWN, (20, ShutterEnums.State.GOING_DOWN), False], 2: [20, 79, ShutterEnums.Direction.DOWN, (20, ShutterEnums.State.GOING_DOWN), False]}.items(): validate(shutter_id, entry) self.pubsub._publish_all_events() time.sleep(50) # Standard shutters will be down now controller._actual_positions[2] = 50 # Simulate position reporting master_controller._update_from_master_state({'module_nr': 0, 'status': 0b00010000}) # Second shutter motor stop # +- actual position # | +- desired position # | | +- direction +- state +- optional skip call check # v v v v v for shutter_id, entry in {0: [25, 99, ShutterEnums.Direction.STOP, (70, ShutterEnums.State.STOPPED), False], 1: [99, 99, ShutterEnums.Direction.STOP, (120, ShutterEnums.State.DOWN)], 2: [50, 79, ShutterEnums.Direction.DOWN, (20, ShutterEnums.State.GOING_DOWN), False]}.items(): validate(shutter_id, entry) time.sleep(10) controller._actual_positions[2] = 50 # Simulate position reporting master_controller._update_from_master_state({'module_nr': 0, 'status': 0b00000000}) # Third motor stopped # +- actual position # | +- desired position # | | +- direction +- state +- optional skip call check # v v v v v for shutter_id, entry in {0: [25, 99, ShutterEnums.Direction.STOP, (70, ShutterEnums.State.STOPPED), False], 1: [99, 99, ShutterEnums.Direction.STOP, (120, ShutterEnums.State.DOWN), False], 2: [50, 79, ShutterEnums.Direction.STOP, (130, ShutterEnums.State.STOPPED)]}.items(): validate(shutter_id, entry) controller._actual_positions[2] = 60 # Simulate position reporting master_controller._update_from_master_state({'module_nr': 0, 'status': 0b00010000}) # Third motor started again # +- actual position # | +- desired position # | | +- direction +- state +- optional skip call check # v v v v v for shutter_id, entry in {0: [25, 99, ShutterEnums.Direction.STOP, (70, ShutterEnums.State.STOPPED), False], 1: [99, 99, ShutterEnums.Direction.STOP, (120, ShutterEnums.State.DOWN), False], 2: [60, 79, ShutterEnums.Direction.DOWN, (130, ShutterEnums.State.GOING_DOWN)]}.items(): validate(shutter_id, entry) controller._actual_positions[2] = 79 # Simulate position reporting master_controller._update_from_master_state({'module_nr': 0, 'status': 0b00000000}) # Third motor stopped again # +- actual position # | +- desired position # | | +- direction +- state +- optional skip call check # v v v v v for shutter_id, entry in {0: [25, 99, ShutterEnums.Direction.STOP, (70, ShutterEnums.State.STOPPED), False], 1: [99, 99, ShutterEnums.Direction.STOP, (120, ShutterEnums.State.DOWN), False], 2: [79, 79, ShutterEnums.Direction.STOP, (130, ShutterEnums.State.DOWN)]}.items(): validate(shutter_id, entry) states = controller.get_states() states['status'].pop(3) # Remove the "unused" shutter states['detail'].pop(3) self.assertDictEqual(states, {'detail': {0: {'actual_position': 25, 'desired_position': 99, 'state': 'stopped', 'last_change': 70}, 1: {'actual_position': 99, 'desired_position': 99, 'state': 'down', 'last_change': 120}, 2: {'actual_position': 79, 'desired_position': 79, 'state': 'down', 'last_change': 130}}, 'status': ['stopped', 'down', 'down']})
def setUpClass(cls): SetTestMode() fakesleep.monkey_patch() fakesleep.reset(seconds=0) cls.test_db = SqliteDatabase(':memory:')
def setUpClass(cls): SetTestMode() fakesleep.monkey_patch() fakesleep.reset(seconds=0)
def test_master_events_and_state(self): fakesleep.reset(0) calls = {} master_communicator = Mock() controller = ShutterController(master_communicator) controller.update_config(ShutterControllerTest.SHUTTER_CONFIG) def shutter_callback(_shutter_id, _shutter_data, _state): calls.setdefault(_shutter_id, []).append([_shutter_data, _state]) controller.set_shutter_changed_callback(shutter_callback) def validate(_shutter_id, _entry): self.assertEquals(controller._actual_positions.get(_shutter_id), _entry[0]) self.assertEquals(controller._desired_positions.get(_shutter_id), _entry[1]) self.assertEquals(controller._directions.get(_shutter_id), _entry[2]) self.assertEquals(controller._states.get(_shutter_id), _entry[3]) if len(_entry) == 4 or _entry[4]: self.assertEqual(calls[_shutter_id].pop()[1], _entry[3][1].upper()) controller.update_from_master_state({ 'module_nr': 0, 'status': 0b00000000 }) for shutter_id in xrange(3): # +- actual position # | +- desired position # | | +- direction +- state +- optional skip call check # v v v v v validate(shutter_id, [ None, None, ShutterController.Direction.STOP, [0, ShutterController.State.STOPPED], False ]) for shutter_id in xrange(3): controller.shutter_down(shutter_id, None) time.sleep(20) controller.update_from_master_state({ 'module_nr': 0, 'status': 0b00010101 }) # +- actual position # | +- desired position # | | +- direction +- state # v v v v for shutter_id, entry in { 0: [ None, None, ShutterController.Direction.DOWN, [20, ShutterController.State.GOING_DOWN] ], 1: [ None, None, ShutterController.Direction.UP, [20, ShutterController.State.GOING_UP] ], 2: [ None, 99, ShutterController.Direction.DOWN, [20, ShutterController.State.GOING_DOWN] ] }.iteritems(): validate(shutter_id, entry) time.sleep(50) # Standard shutters will be down now controller._actual_positions[2] = 20 # Simulate position reporting controller.update_from_master_state({ 'module_nr': 0, 'status': 0b00010100 }) # First shutter motor stop # +- actual position # | +- desired position # | | +- direction +- state +- optional skip call check # v v v v v for shutter_id, entry in { 0: [ None, None, ShutterController.Direction.STOP, [70, ShutterController.State.STOPPED] ], 1: [ None, None, ShutterController.Direction.UP, [20, ShutterController.State.GOING_UP], False ], 2: [ 20, 99, ShutterController.Direction.DOWN, [20, ShutterController.State.GOING_DOWN], False ] }.iteritems(): validate(shutter_id, entry) time.sleep(50) # Standard shutters will be down now controller._actual_positions[2] = 50 # Simulate position reporting controller.update_from_master_state({ 'module_nr': 0, 'status': 0b00010000 }) # Second shutter motor stop # +- actual position # | +- desired position # | | +- direction +- state +- optional skip call check # v v v v v for shutter_id, entry in { 0: [ None, None, ShutterController.Direction.STOP, [70, ShutterController.State.STOPPED], False ], 1: [ None, None, ShutterController.Direction.STOP, [120, ShutterController.State.UP] ], 2: [ 50, 99, ShutterController.Direction.DOWN, [20, ShutterController.State.GOING_DOWN], False ] }.iteritems(): validate(shutter_id, entry) time.sleep(10) controller._actual_positions[2] = 50 # Simulate position reporting controller.update_from_master_state({ 'module_nr': 0, 'status': 0b00000000 }) # Third motor stopped # +- actual position # | +- desired position # | | +- direction +- state +- optional skip call check # v v v v v for shutter_id, entry in { 0: [ None, None, ShutterController.Direction.STOP, [70, ShutterController.State.STOPPED], False ], 1: [ None, None, ShutterController.Direction.STOP, [120, ShutterController.State.UP], False ], 2: [ 50, 99, ShutterController.Direction.STOP, [130, ShutterController.State.STOPPED] ] }.iteritems(): validate(shutter_id, entry) controller._actual_positions[2] = 60 # Simulate position reporting controller.update_from_master_state({ 'module_nr': 0, 'status': 0b00010000 }) # Third motor started again # +- actual position # | +- desired position # | | +- direction +- state +- optional skip call check # v v v v v for shutter_id, entry in { 0: [ None, None, ShutterController.Direction.STOP, [70, ShutterController.State.STOPPED], False ], 1: [ None, None, ShutterController.Direction.STOP, [120, ShutterController.State.UP], False ], 2: [ 60, 99, ShutterController.Direction.DOWN, [130, ShutterController.State.GOING_DOWN] ] }.iteritems(): validate(shutter_id, entry) controller._actual_positions[2] = 99 # Simulate position reporting controller.update_from_master_state({ 'module_nr': 0, 'status': 0b00000000 }) # Third motor stopped again # +- actual position # | +- desired position # | | +- direction +- state +- optional skip call check # v v v v v for shutter_id, entry in { 0: [ None, None, ShutterController.Direction.STOP, [70, ShutterController.State.STOPPED], False ], 1: [ None, None, ShutterController.Direction.STOP, [120, ShutterController.State.UP], False ], 2: [ 99, 99, ShutterController.Direction.STOP, [130, ShutterController.State.DOWN] ] }.iteritems(): validate(shutter_id, entry) states = controller.get_states() states['status'].pop(3) # Remove the "unused" shutter states['detail'].pop(3) self.assertEqual( states, { 'detail': { 0: { 'actual_position': None, 'desired_position': None, 'state': 'stopped' }, 1: { 'actual_position': None, 'desired_position': None, 'state': 'up' }, 2: { 'actual_position': 99, 'desired_position': 99, 'state': 'down' } }, 'status': ['stopped', 'up', 'down'] })